【iOS 】—— 单例模式,通知模式和代理模式
1. 单例模式
什么是单例模式
单例模式在整个工程中,相当于是全局变量,就是不论在哪里需要用到这个类的实例变量,都可以通过单例方法来取得,一旦创建一个单例类,不论在哪里调用这个单例方法,创建的所有对象都指向同一块内存地址。(即单例类保证了该类的实例对象是唯一存在的一个)
优缺点:
优点:
- 单例模式可以保证系统中一个类只有一个实例并且该实例易于访问。从而方便对实例个数控制并节约资源。
- 单例控制了类实例化的进程,所以类可以更加灵活的实例化。
缺点:
- 单例对象一旦建立,对象的指针保存在静态区,单例对象分配在堆上的内存知道程序结束才会释放。
- 单例类无法被继承,很难进行类的扩展。
- 单例不适合变化的对象,如果通一个对象总要在不同的用例场景变化,单例就回引起数据错误。
实现方式
懒汉模式
实现原理和懒加载很像,如果程序中不使用这个对象,就不会创建,只有在使用代码创建这个对象,才会创建。(其实就是在第一次使用单例的时候才进行初始化)
如果要保证应用中就只有一个对象,就应该让类的alloc
方法只会进行一次内存空间的分配。所以我们需要重写alloc
方法,这里提供了两种方法:一种是alloc
,一种是allocWithZone
方法。
- 在alloc调用的底层就是allocWithZone方法,因此只需要重写allocWithZone方法。
id manager;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
if (manager == nil) {
// 调用super的allocWithZone方法来分配内存空间
manager = [super allocWithZone:zone];
}
return manager;
}
涉及到多线程时,需要对其进行加锁,来避免**(一个线程进if
判断为空,然后进去创建对象,在还没有返回的时候,另一条线程又进入if
判断,判断仍然为空,再次进入,导致保证不了只有一个单例对象)**
然后加上锁
id manager;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
// 在这里加一把锁(利用本类为锁)进行多线程问题的解决
@synchronized(self){
if (manager == nil) {
// 调用super的allocWithZone方法来分配内存空间
manager = [super allocWithZone:zone];
}
}
return manager;
}
这样的话就可以解决多线程并发的问题,但是每次进行alloc是时候都会加锁和判断锁的存在,这也是可以优化的:
id manager;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
// 在这里判断,为了优化资源,防止多次加锁和判断锁
if (manager == nil) {
// 在这里加一把锁(利用本类为锁)进行多线程问题的解决
@synchronized(self){
if (manager == nil) {
// 调用super的allocWithZone方法来分配内存空间
manager = [super allocWithZone:zone];
}
}
}
return manager;
}
这样的话在判断锁或加锁之前多加一条单例对象是否为空的判断就可以省去多余的锁判断,但是这样的话还不够优化,我们还可以使用GCD
方法进行一个优化:
dispatch_once
一次性代码(只会执行一次):
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//此处编写只执行一次的代码(这里main默认是线程安全的)
});
同时对manager应该使用static以避免被extern进行了操作
这里我们浅讲一下static和extern的区别:
- static:定义在变量或函数前面,它的含义是改变默认的external链接属性,使它们的作用域限定在本文件内部,这样其他类的文件就不能对它做引用和修改了,保证了单例的一个安全性。
- extern:修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用,并可以对该变量/函数进行修改”
加了static的GCD版的代码例子如下:
//加了static限制其作用域,保证其不会被引用它的其他类修改
static id manager;
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [super allocWithZone:zone];
});
return manager;
}
我们也可以通过自己定义一个初识单例的类方法作为外部创建对象的借口:
static id manager;
//自定义创建单例的类方法接口
+ (instancetype)sharedManger{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [super allocWithZone:zone];
});
return manager;
}
//但是这时候我们还是得确保安全去重写一下allocWithZone方法,否则外部如果采用alloc的方法来创建单例对象的时候就会每alloc一遍就会新创建一个该对象,单例就失去了意义
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
if (manager == nil) {
manager = [super allocWithZone:zone];
}
return manager;
}
举一个真实的懒汉式创建单例的例子:
先创建一个继承于NSObject
的类manager
作为单例类
接下来就是第一种情况的实现代码:
//Manager.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Manager : NSObject
- (void)print;
+ (instancetype)sharedManager;
@end
NS_ASSUME_NONNULL_END
//Manager.m
#import "Manager.h"
static id myManager;
@implementation Manager
- (void)print {
NSLog(@"调用了单例对象");
}
+ (instancetype)sharedManager {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
myManager = [[Manager alloc] init];
NSLog(@"使用GCD创建单例");
});
return myManager;
}
+ (instancetype) allocWithZone:(struct _NSZone *)zone {
myManager = [super allocWithZone: zone];
NSLog(@"调用alloc重现创建一个单例");
return myManager;
}
@end
//ViewController.m
#import "ViewController.h"
#import "Manager.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[[Manager sharedManager] print];
[[Manager alloc] print];
}
@end
运行结果:
调用alloc
方法的时候仍然重新创建了一个Manager对象。这与单例的规定不符。
//Manager.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Manager : NSObject
- (void)print;
+ (instancetype)sharedManager;
@end
NS_ASSUME_NONNULL_END
//Manager.m
#import "Manager.h"
static id myManager;
@implementation Manager
- (void)print {
NSLog(@"调用了单例对象");
}
+ (instancetype)sharedManager {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
myManager = [[Manager alloc] init];
NSLog(@"使用GCD创建单例");
});
return myManager;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
if (myManager == nil) {
myManager = [super allocWithZone: zone];
NSLog(@"调用alloc重新创建一个单例");
}
return myManager;
}
@end
//ViewController.m
#import "ViewController.h"
#import "Manager.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[[Manager sharedManager] print];
[[Manager alloc] print];
}
@end
运行结果:
可以看到即使后面再调用alloc
方法来创建单例,没有去重复创建,而是直接使用之前创建的对象。
饿汉模式
在使用代码创建对象之前就创建好了对象。
load
方法:当类加载到运行环境的时候就调用且仅调用一次,同时一个类只会加载一次。(类加载有别于引用类,所有类都会在程序启动的时候加载一次。)initialize
方法:当第一次使用类的时候加载且仅加载一次。
两者相比较:
- 在不考虑开发者主动使用的情况下,系统最多调用一次。
- 如果父类和子类都被调用,父类的调用一定在子类之前。
- 都是为了程序的运行前创建的运行环境。
它们的相同点在于:方法只会被调用一次。
load
是只要类所在文件被引用就会调用。initialize
是在类或者子类的第一个方法被调用前调用。所以如果类没有被引用进项目,就不会有 load
调用;但即使类文件被引用进来,但是没有使用,那么 initialize
也不会被调用。
父类(Superclass)
的方法优先于子类(Subclass)
的方法,类中的方法优先于分类(Category)
中的方法。
饿汉模式代码:
static id manager;
+ (void)load {
//只会加载一次也就不需要加锁
manager = [[self alloc] init];
}
+ (instancetype)sharedManger{
if (manager == nil) {
manager = [super allocWithZone:zone];
}
return. manager;
}
//但是这时候我们还是得确保安全去重写一下allocWithZone方法,否则外部如果采用alloc的方法来创建单例对象的时候就会每alloc一遍就会新创建一个该对象,单例就失去了意义
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
if (manager == nil) {
manager = [super allocWithZone:zone];
}
return manager;
}
由于在类被加载的时候会调用且调用一次load方法,而load方法里面又调用了alloc方法进行对象的初始化,所以,第一次调用肯定是已经创建好了对象,而且这时候不会存在多线程的问题。
再一个需要我们关注的重点就是: 重写allocWithZone:方法的问题,由于alloc方法中也是调用的allocWithZone:方法,所以我们重写allocWithZone:方法,在其中添加判断等操作是最有效的控制只执行初始化一次的手段,因为如果不重写allocWithZone:方法的话,如果外面的类创建单例对象的时候没有用到类似于sharedManger方法的我们自定义的方法接口,而是直接用了alloc等系统的初始化接口,就会导致重复初始化单例对象的现象,使单例失去意义。
我们能够保证只去调用自定义的接口创建单例的话,也可以不去重写allocWithZone:
方法。
//Manager.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface eManager : NSObject
- (void)print;
+ (instancetype)sharedManager;
@end
NS_ASSUME_NONNULL_END
//Manager.m
#import "eManager.h"
static id myManager;
@implementation eManager
+ (void)load {
myManager = [[eManager alloc] init];
}
- (void)print {
NSLog(@"调用了单例对象");
}
+ (instancetype)sharedManager {
if (myManager == nil) {
myManager = [[eManager alloc] init];
}
return myManager;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
if (myManager == nil) {
myManager = [super allocWithZone:zone];
NSLog(@"调用alloc重新创建一个单例");
}
return myManager;
}
@end
//ViewController.m
#import "ViewController.h"
#import "eManager.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
//饿汉模式
[[eManager sharedManager] print];
[[eManager alloc] print];
}
@end
运行结果:
2. 通知模式
概要
- 观察者和被观察者都无需知晓对方,只需要通过标记在NSNotificationCenter中找到监听该通知所对应的类,从而调用该类的方法。
- 并且在NSNotificationCenter中,观察者可以只订阅某一特定的通知,并对齐做出相应操作,而不用对某一个类发的所有通知都进行更新操作。
- NSNotificationCenter对观察者的调用不是随机的,而是遵循注册顺序一一执行的,并且在该线程内是同步的
通知的具体使用流程:
- 创建通知对象:
NSNotification *notice = [NSNotification notificationWithName:@"send" object:self userInfo:@{@"name":_renameTextField.text,@"pass":_repassTextField.text}];
- 通知中心发送通知
[[NSNotificationCenter defaultCenter] postNotification:notice];
- 注册通知,添加观察者来指定一个方法,名称,对象,接收到的通知时执行指定的方法。
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(recive:) name:@"send" object:nil];
- 接受通知后调用的方法
- (void)recive:(NSNotification *)notice {
NSDictionary *dictionary = notice.userInfo;
_nameTextField.text = dictionary[@"name"];
_passTextField.text = dictionary[@"pass"];
}
总结一下用法:
- 接受通知的类注册监听者,并实现接受通知的时间函数。
- 触发通知的类在适当的时候发送通知。
通知底层实现原理
NSNotification
NSNotification
包含了消息发送的一些信息,包括name
消息名称、object
消息发送者、userinfo
消息发送者携带的额外信息,其类结构如下:
@interface NSNotification : NSObject <NSCopying, NSCoding>
@property (readonly, copy) NSNotificationName name;
@property (nullable, readonly, retain) id object;
@property (nullable, readonly, copy) NSDictionary *userInfo;
- (instancetype)initWithName:(NSNotificationName)name object:(nullable id)object userInfo:(nullable NSDictionary *)userInfo API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)) NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
@end
@interface NSNotification (NSNotificationCreation)
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject;
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
- (instancetype)init /*API_UNAVAILABLE(macos, ios, watchos, tvos)*/; /* do not invoke; not a valid initializer for this class */
@end
可以通过实例方法创建NSNotification
对象,也可以通过类方法构建:
NSNotificationCenter:消息中心
这个单例类中主要定义了两个表,一个存储所有注册通知信息的表的结构体,一个保存单个注册信息的节点结构体。
typedef struct NCTbl {
Observation *wildcard; // 添加观察者时既没有传入 NotificationName ,又没有传入object,就会加在这个链表上,它里边的观察者可以接收所有的系统通知
GSIMapTable nameless; // 添加观察者时没有传入 NotificationName 的表
GSIMapTable named; // 添加观察者时传入了 NotificationName 的表
} NCTable
保存观察者的信息
typedef struct Obs {
id observer; // 观察者对象
SEL selector; // 方法信息
struct Obs *next; // 指向下一个节点
int retained; /* Retain count for structure. */
struct NCTbl *link; /* Pointer back to chunk table */
} Observation;
named表
在named表中,NotifcationName 作为表的 key,因为我们在注册观察者的时候是可以传入一个参数 object 用于只监听指定该对象发出的通知,并且一个通知可以添加多个观察者,所以还需要一张表来保存 object 和 Observer 的对应关系。这张表的是 key、Value 分别是以 object 为 Key,Observer 为 value用了链表这种数据结构实现保存多个观察者的情况。
在实际开发过程中 object 参数我们经常传 nil,这时候系统会根据 nil 自动生成一个 key,相当于这个 key 对应的 value(链表)保存的就是当前通知传入了 NotificationName 没有传入 object 的所有观察者。
nameless
他在注册时没有NotificationName
,即没有最外边一层键值对,其中就只有 object 和 Observation 所对应的键值对结构了:
wildcard表
这个表既没有 NotificationName 也没有 object 了,所以他就会在 nameless基础上在脱去一层键值对,那么它就只剩下一个链表了,该练表存储了可以接收所有通知的类的信息。
NSNotificationQueue
通知队列,用于异步发送消息,这个异步并不是开启线程,而是把通知存到双向链表实现的队列里面,等待某个时机触发时调用NSNotificationCenter的发送接口进行发送通知,这么看NSNotificationQueue最终还是调用NSNotificationCenter进行消息的分发。
NSNotificationQueue主要做两件事:
- 添加通知到队列。
- 删除通知。
// 把通知添加到队列中,NSPostingStyle是个枚举,下面会介绍
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
// 删除通知,把满足合并条件的通知从队列中删除
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;
把通知添加到队列等待发送,同时提供了一些附加条件供开发者选择,如:什么时候发送通知、如何合并通知等,系统给了如下定义:
// 表示通知的发送时机
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
NSPostWhenIdle = 1, // runloop空闲时发送通知
NSPostASAP = 2, // 尽快发送,这种情况稍微复杂,这种时机是穿插在每次事件完成期间来做的
NSPostNow = 3 // 立刻发送或者合并通知完成之后发送
};
// 通知合并的策略,有些时候同名通知只想存在一个,这时候就可以用到它了
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
NSNotificationNoCoalescing = 0, // 默认不合并
NSNotificationCoalescingOnName = 1, // 只要name相同,就认为是相同通知
NSNotificationCoalescingOnSender = 2 // object相同
};
注册通知
使用方法addObserver:selector:name:object
添加观察者,根据 GNUStep 的源码分析:
- (void) addObserver: (id)observer
selector: (SEL)selector
name: (NSString*)name
object: (id)object
{
Observation *list;
Observation *o;
GSIMapTable m;
GSIMapNode n;
// observer为空时的报错
if (observer == nil)
[NSException raise: NSInvalidArgumentException
format: @"Nil observer passed to addObserver ..."];
// selector为空时的报错
if (selector == 0)
[NSException raise: NSInvalidArgumentException
format: @"Null selector passed to addObserver ..."];
// observer不能响应selector时的报错
if ([observer respondsToSelector: selector] == NO)
{
[NSException raise: NSInvalidArgumentException
format: @"[%@-%@] Observer '%@' does not respond to selector '%@'",
NSStringFromClass([self class]), NSStringFromSelector(_cmd),
observer, NSStringFromSelector(selector)];
}
// 给表上锁
lockNCTable(TABLE);
// 建立一个新Observation,存储这次注册的信息
o = obsNew(TABLE, selector, observer);
// 如果有name
if (name) {
// 在named表中 以name为key寻找value
n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
// named表中没有找到对应的value
if (n == 0) {
// 新建一个表
m = mapNew(TABLE);
// 由于这是对给定名称的首次观察,因此我们对该名称进行了复制,以便在map中无法对其进行更改(来自GNUStep的注释)
name = [name copyWithZone: NSDefaultMallocZone()];
// 新建表作为name的value添加在named表中
GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
GS_CONSUMED(name)
} else { //named表中有对应的value
// 取出对应的value
m = (GSIMapTable)n->value.ptr;
}
// 将observation添加到正确object的列表中
// 获取添加完后name对应的value的object对应的链表
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
// n是object的value
if (n == 0) { // 如果object对应value没有数据
o->next = ENDOBS;
// 将o作为object的value链表的头结点插入
GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
} else { // 如果有object对应的value那么就直接添加到原练表的尾部
// 在链表尾部加入o
list = (Observation*)n->value.ptr;
o->next = list->next;
list->next = o;
}
// 这个else if 就是没有name有object的Observation,对object进行的操作相同,
} else if (object) {
// 直接获取object对应的value链表
n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
if (n == 0) { // 这个对应链表如果没有数据
o->next = ENDOBS;
// 将该observation作为头节点插入
GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
} else { // 有数据,将obsevation直接插在原链表的后面
list = (Observation*)n->value.ptr;
o->next = list->next;
list->next = o;
}
} else {
// 既没有name又没有object,就加在WILDCARD链表中
o->next = WILDCARD;
WILDCARD = o;
}
// 解锁
unlockNCTable(TABLE);
}
NCTable
结构体中核心的三个变量以及功能:wildcard
、named
、nameless
,在源码中直接用宏定义表示了:WILDCARD
、NAMELESS
、NAMED
。
Case1:存在name(无论object是否存在)
- 注册通知,如果通知的name存在,以
name
为key从named
字典中取出值n
,这个n
还是个字典。 - 然后以
object
为key,从字典n
中取出对应的值,这个值就是Observation
类型(后面简称obs
)的链表,然后把刚开始创建的obs
对象o
存储进去。
如果注册通知时传入name
,那么会是一个双层的存储结构
- 找到
NCTable
中的named
表,这个表存储了还有name
的通知。 - 以
name
作为key,找到value
,这个value
依然是一个map
。 map
的结构是以object
作为key,obs
对象为value,这个obs
对象的结构上面已经解释,主要存储了observer & SEL
。
Case2:只存在object
- 以
object
为key,从nameless
字典中提取value
,此value
是个obs类型的链表。 - 把创建的
obs
类型的对象o
存储到链表中。
只存在object
时存储只有一层,那就是object
和obs
对象之间的映射。
Case3:没有name和object
这种情况直接把obs
对象存放在了Observation *wildcard
链表结构中。
总结:
- 存储是以name和object为维度,判断是不是同一个通知要从name和object区分。
- 存储过程并没有做去重操作,这也解释了为什么同一个通知注册多次则响应多次。
发送通知
使用方法postNotification
:, postNotificationName:object:userInfo
或者postNotificationName:object:
发送通知,后者默认userInfo
为nil
发送通知的核心逻辑比较简单,基本上就是查找和调用响应方法,核心函数如下:
// 发送通知
- (void) postNotification: (NSNotification*)notification {
if (notification == nil) {
[NSException raise: NSInvalidArgumentException
format: @"Tried to post a nil notification."];
}
[self _postAndRelease: RETAIN(notification)];
}
- (void) postNotificationName: (NSString*)name
object: (id)object {
[self postNotificationName: name object: object userInfo: nil];
}
- (void) postNotificationName: (NSString*)name
object: (id)object
userInfo: (NSDictionary*)info
{
// 构造一个GSNotification对象, GSNotification继承了NSNotification
GSNotification *notification;
notification = (id)NSAllocateObject(concrete, 0, NSDefaultMallocZone());
notification->_name = [name copyWithZone: [self zone]];
notification->_object = [object retain];
notification->_info = [info retain];
// 进行发送操作
[self _postAndRelease: notification];
}
上面的方法最终都只会调用 _postAndRelease:
方法。不同的是,postNotification:
方法外部直接传了一个NSNotification
对象,其他两个方法都是内部进行了处理包装 成为了一个NSNotification
对象,
//发送通知的核心函数,主要做了三件事:查找通知、发送、释放资源
- (void) _postAndRelease: (NSNotification*)notification {
//step1: 从named、nameless、wildcard表中查找对应的通知
...
//step2:执行发送,即调用performSelector执行响应方法,从这里可以看出是同步的
[o->observer performSelector: o->selector
withObject: notification];
//step3: 释放资源
RELEASE(notification);
}
总共分为:查找通知,发送,释放资源。
查找通知:
- 创建
observerArray
数组来存储需要保存的通知observer
。 - 遍历
wildcard
链表,将observer
添加到observerArray
数组中。 - 若存在
object
,在nameless table
中以object
为key的链表然后遍历链表,找到后将observer
存储到observerArray
数组中。 - .若存在
NotificationName
,在named table
中以NotificationName
为 key 找到对应的 table,然后再在找到的 table 中以object
为 key 找到对应的链表,遍历链表,将observer
添加到observerArray
数组中。如果object
为nil,则以 nil 为 key 找到对应的链表,遍历链表,将observer
添加到observerArray
数组中。 - 至此所有关于当前通知的
observer
(wildcard + nameless + named
)都已经加入到了数组observerArray
中。
发送通知
取出其中 的observer 节点(包含了观察者对象和 selector)通过performSelector:
逐一调用sel
,这是个同步操作
释放资源:
释放notification
对象
发送过程的概述:从三个存储容器中:named
、nameless
、wildcard
去查找对应的obs
对象,然后通过performSelector:
逐一调用响应方法,这就完成了发送流程。
删除通知
要注意的点:
- 查找时仍然以
name
和object
为维度的,再加上observer
做区分。 - 因为查找时做了这个链表的遍历,所以删除时会把重复的通知全都删除掉。
// 删除已经注册的通知
- (void) removeObserver: (id)observer
name: (NSString*)name
object: (id)object {
if (name == nil && object == nil && observer == nil)
return;
...
}
- (void) removeObserver: (id)observer
{
if (observer == nil)
return;
[self removeObserver: observer name: nil object: nil];
}
name和object都不存在
清空 wildcard
链表。
name不存在(不管object存不存在)
遍历 nameless table
,若 object 为 nil,则清空 nameless table
,若 object 不为 nil,则以 object 为 key 找到对应的链表,然后清空链表。在 nameless table
中以 object 为 key 找到对应的 observer
链表,然后清空,若 object 也为 nil,则清空 nameless table
。
name存在
在 named table 中以 NotificationName 为 key 找到对应的 table,若 object 为 nil,则清空找到的 table,若 object 不为 nil,则以 object 为 key 在找到的 table 中取出对应的链表,然后清空链表。
异步通知
异步通知机制通过NSNotificationQueue
的异步发送,从线程的角度看并不是真正的异步发送,或可称为延时发送,它是利用了runloop
的时机来触发的
入队
/*
* 把要发送的通知添加到队列,等待发送
* NSPostingStyle 和 coalesceMask在上面的类结构中有介绍
* modes这个就和runloop有关了,指的是runloop的mode
*/
- (void) enqueueNotification: (NSNotification*)notification
postingStyle: (NSPostingStyle)postingStyle
coalesceMask: (NSUInteger)coalesceMask
forModes: (NSArray*)modes
{
......
// 判断是否需要合并通知
if (coalesceMask != NSNotificationNoCoalescing) {
[self dequeueNotificationsMatching: notification
coalesceMask: coalesceMask];
}
switch (postingStyle) {
case NSPostNow: {
...
// 如果是立马发送,则调用NSNotificationCenter进行发送
[_center postNotification: notification];
break;
}
case NSPostASAP:
// 添加到_asapQueue队列,等待发送
add_to_queue(_asapQueue, notification, modes, _zone);
break;
case NSPostWhenIdle:
// 添加到_idleQueue队列,等待发送
add_to_queue(_idleQueue, notification, modes, _zone);
break;
}
}
- 根据
coalesceMask
参数判断是否合并通知 - 接着根据
postingStyle
参数,判断通知发送的时机,如果不是立即发送则把通知加入到队列中:_asapQueue
、_idleQueue
发送通知
static void notify(NSNotificationCenter *center,
NSNotificationQueueList *list,
NSString *mode, NSZone *zone)
{
......
// 循环遍历发送通知
for (pos = 0; pos < len; pos++)
{
NSNotification *n = (NSNotification*)ptr[pos];
[center postNotification: n];
RELEASE(n);
}
......
}
// 发送_asapQueue中的通知
void GSPrivateNotifyASAP(NSString *mode)
{
notify(item->queue->_center,
item->queue->_asapQueue,
mode,
item->queue->_zone);
}
// 发送_idleQueue中的通知
void GSPrivateNotifyIdle(NSString *mode)
{
notify(item->queue->_center,
item->queue->_idleQueue,
mode,
item->queue->_zone);
}
runloop
触发某个时机,调用GSPrivateNotifyASAP()
和GSPrivateNotifyIdle()
方法,这两个方法最终都调用了notify()
方法notify()所做的事情就是调用NSNotificationCenter
的postNotification:
进行发送通知
主线程响应通知
异步线程发送通知则响应函数也是在异步线程,如果执行UI刷新相关的话就会出问题,那么如何保证在主线程响应通知呢?
其实也是比较常见的问题了,基本上解决方式如下几种:
- 使用addObserverForName: object: queue: usingBlock方法注册通知,指定在mainqueue上响应block
- 在主线程注册一个machPort,它是用来做线程通信的,当在异步线程收到通知,然后给machPort发送消息,这样肯定是在主线程处理的
总结:
注册通知:
- 在同步通知中,存储以name和object为维度,判定是不是同一个通知要从name和object区分,如果都相同则认为是同一个通知,后续的查找逻辑,删除逻辑都是以这两个维度。
- 通过name和object将通知存储划分为三种结构:named表(name存在)、nameless表(name不存在)、wildcard链表(name和object都不存在)。
- 在通知的存储过程并没有做去重操作,这也解释了为什么同一个通知注册多次则响应多次。
发送通知
- 首先创建一个数组用来存储接受通知的观察者,接着根据name和object作区分来遍历wildcard链表、nameless表、named表。
- 遍历所有表,意味着注册多次通知就会响应多次。
- 通过
performSelector:
逐一调用sel
进行发送,这是个同步操作。 - 接收通知的线程,和发送通知所处的线程是同一个线程。也就是说如果要在接收通知的时候更新 UI,需要注意发送通知的线程是否为主线程。
删除通知:
- 查找时仍然以
name
和object
为维度的,再加上observer
做区分。 - 因为查找时做了这个链表的遍历,所以删除时会把重复的通知全都删除掉。
异步通知:
依赖runloop,所以如果在其他子线程使用NSNotificationQueue,需要开启runloop
最终还是通过NSNotificationCenter进行发送通知,所以这个角度讲它还是同步的
所谓异步,指的是非实时发送而是在合适的时机发送,并没有开启异步线程
页面销毁时不移除通知会崩溃吗?
在观察者对象释放之前,需要调用removeOberver方法将观察者从通知中心移除,否则程序可能会出现崩溃。但是在iOS9之后,通知中心对观察者的持有由unsafe_unretained 引用变为weak引用。即使不对观察者手动移除,持有观察者的引用也会在观察者回收之后置空。但是通过 addObserverForName:object: queue:usingBlock:
方法注册的观察者需要手动释放,因为通知中心持有的是它们的强引用。
3. 代理模式
原理:
代理传值其实就是利用了可以在遵守代理协议的视图中实现代理方法的原理,上述例子中,我们自己先定义了一个代理协议,然后在B中定义一个代理对象delegate,同时在需要传值的地方实现了代理对象delegate对代理方法的调用,好了,关键来了,这个代理对象delegate其实就是代理传值的核心,因为我们在A视图中创建B视图的时候有行这样的代码:
self.myView.delegate = self;
在此之前,B视图已经初始化创建出来了,同时你将A视图自身的地址赋值给了B视图中的delegate属性,那么此时B视图中的delegate属性是不是就等同于是A视图了,并且该delegate还遵循了代理协议,那不就是等同于遵循该代理协议的A视图嘛,那不就有了self.delegate == 遵循协议的A视图控制器这不就是完全成立的吗,现在我们在看在B视图中写的回传值的方法:
[self.delegate changeUILabelText:self.myTextField.text];
代理传值就是将一个视图的地址传递给另一个视图的delegate属性,使用该属性进行操作其实就是使用那个视图在进行操作。
代理的循环引用
为什么要对delegate使用weak修饰符?
发送方的delegate
等同于接收方,其实就是它引用了接收方视图,并且我们在接收方中定义发送方视图的时候,接收方这不又引用了发送方,两个视图之间互相引用,就会形成循环引用。因此,才在发送方的delegate
使用weak
进行修饰,即发送方delegate
属性弱引用接收方,接收方强引用发送方,这不就打破了循环引用。
小结:
代理和通知的区别
- 效率:代理比通知高;
- 关联:代理是强关联,委托和代理双方互相知道。通知是弱关联,不需要知道是谁发,也不需要知道是谁接收;
- 代理是一对一的关系,通知是一对多的关系;代理要实现对多个类发出消息可以通过将代理者添加入集合类后遍历,或通过消息转发来实现。
- 代理一般行为需要别人来完成,通知是全局通知;
KVO和通知的区别
- 相同:都是一对多的关系;
- 不同:通知是需要被观察者先主动发出通知,观察者注册监听再响应,比KVO多了发送通知这一步;
- 监听范围:KVO是监听一个值的变化,通知不局限于监听属性的变化,还可以对多种多样的状态变化进行监听,通知的监听范围广,使用更灵活;
- 使用场景:KVO的一般使用场景是监听数据变化,通知是全局通知;
block和代理的区别
-
相同点:
- block和代理都是回调的方式。使用场景相同。
-
不同点:
- block集中代码块,而代理分散代码块,所以 block 更适用于轻便、简单的回调,如网络传输,代理适用于公共接口较多的情况,这样做也更易于解耦代码架构;
- block运行成本高,block出栈时,需要将使用的数据从栈内存拷贝到堆内存。当然如果是对象就是加计数,使用完或block置为 nil 后才消除,而代理只是保存了一个对象指针,直接回调,并没有额外消耗,相对C的函数指针,只是多做了一个查表动作;
设计模式总结:
KVO和通知------->观察者模式
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
优势:解耦合
接口隔离原则、开放-封闭原则。
KVC --------> KVC模式
单例模式
利用应用程序只有一个该类的实例对象这一特殊性来实现资源共享。
优势:使用简单,延时求值,易于跨模块
劣势:这块内存知道程序退出时才能释放
单一职责原则
举例:[UIApplication sharedApplication]。
代理模式
委托方将不想完成的任务交给代理方处理,并且需要委托方通知代理方才能处理。
优势: 解耦合
开放-封闭原则
举例:tableview的数据源和代理
MVC模式
将程序书写分为三层,分别为模型、视图、控制器,每层都有各自的职责完成各自的工作。
优势: MVC模式使系统,层次清晰,职责分明,易于维护
对扩展开放-对修改封闭
MVVM模式
用于解决MVC模式下C层代码冗杂的情况(过多的网络请求以及业务逻辑处理)而出现的MVVM模式,其相比于MVC多了一层ViweModel(业务处理和数据转化)层,专门用于处理数据。
当功能简单时,MVVM反而会增加很多代码,所以对于简单的功能,MVC更加的方便。
三种工厂模式
通过给定参数来返回对应的实例,完全对用户隐藏其实现的原理。
优势:易于替换,面向抽象编程
依赖倒置原则