NSNotification
通知机制的核心:通知中心
(NSNotificationCenter)
是一个与线程关联的【单例对象】
单例:通知中心是个单例
通知中心发送通知给观察者是同步的,也可以用通知队列
(NSNotificationQueue)异步发送通知
方法:
add,post,remove
// 添加通知观察者
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable
NSNotificationName)aName object:(nullable id)anObject;
- (id <NSObject>)addObserverForName:(nullable
NSNotificationName)name object:(nullable id)obj queue:(nullable
NSOperationQueue *)queue usingBlock:(void (^)(NSNotification
*note))block;
// 发出通知
- (void)postNotification:(NSNotification *)notification;
- (void)postNotificationName:(NSNotificationName)aName object:
(nullable id)anObject;
- (void)postNotificationName:(NSNotificationName)aName object:
(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
// 移除通知观察者
- (void)removeObserver:(id)observer;
- (void)removeObserver:(id)observer name:(nullable
NSNotificationName)aName object:(nullable id)anObject;
参数:
notificationName:为nil
通知中心会通知所有与该通知中object相匹配的监听对象。
若anObject为 nil
通知中心会通知所有与该通知中notificationName相匹配的监听对象
移除观察者
1. 不需要移除
从iOS9开始,即使不移除观察者对象,程序也不会出现异常。
观察者由==unsafe_unretained==引用变为==weak==引用
2. 需要移除
通过`addObserverForName:object: queue:usingBlock:`方法注册的观察者需
要手动释放,因为通知中心持有的是它们的强引用。
NSNotificationQueue
多线程
1.通知的发送和接收都是在 同一个线程:
无论在哪个线程中注册了观察者,通知的发送和接收都是在同一个线程中
2. 通知的接收 所在线程 与 注册通知所在线程无关
接收线程 == 发送线程
在主线程注册了观察者,然后在子线程发送通知,最后接收和处理通知也是在子
线程。一般情况下,发送通知所在的线程就是接收通知所在的线程。
指定接收线程
可以统一所有接收通知后在主线程回调,也可以不封装,由各业务同学自己根据
情况处理
实现原理:
NSConcreteNotification
`NSNotification`是一个类蔟不能够实例化,当我们调用
`initWithName:object:userInfo:`方法的时候,系统内部会实例化
`NSNotification`的子类`NSConcreteNotification`。在这个子类中重写了
`NSNofication`定义的相关字段和方法。
NSNotificationCenter
`NSNotificationCenter`是通知的管理类,实现较复杂。
`NSNotificationCenter`中主要定义了两个table,同时也定义了
`Observation`保存观察者信息
关键对象
NotificationName
通知名称
observer
观察者
object
监听者
通知设计是根据这几个参数对象的有无来决定的
通知存取原理
注册通知就是将对应信息和对象映射到表 或者 链表中,取值也是根据响应key'去
取
typedef struct NCTbl {
GSIMapTable named; /* 有通知名称 的哈希表 */
GSIMapTable nameless; /* 没有通知名称,有object 的表 */
Observation *wildcard; /* 没有name和object 的链表 */
unsigned lockCount; /* Count recursive operations. */
NSRecursiveLock *_lock; /* Lock out other threads. */
Observation *freeList;
Observation **chunks;
unsigned numChunks;
GSIMapTable cache[CACHESIZE];
unsigned short chunkIndex;
unsigned short cacheIndex;
} NCTable;
从上面结构可以看出,数据映射表设计依赖参数有无
object为空的,内部自动生成对应的key
表named原理示意图
有通知名
named 表结构
key
notificationName1
1
notificationName2
2
...
3
value
objTable1
1
objTable2
2
...
3
named 表中value对应object 表:
objTable1 表结构【nameless 表】
key
object 1
1
object 2
2
...
3
value
obsever 链表 1
1
obsever 链表 2
2
...
3
object 表 对应observer 观察者链表:
【wildcard 链表】:head
observer
--------
next
observer
--------
next
...
null
nameless 表:
就是上面 object 表
wildcard 链表:
就是上面的 observer 链表
observer 结构:
typedef struct Obs {
id observer; /* 接收通知的对象(注册者). */
SEL selector; /* 接收通知调用的方法 */
struct Obs *next; /* 通知名和object相同时,obs链表下一个obsver
*/
int retained; /* Retain count for structure. */
struct NCTbl *link; /* Pointer back to chunk table */
} Observation;
添加观察者流程
所有注册观察者的操作最后都会调用`addObserver:selector:name:object:`
1. 首先会根据传入的参数实例化一个Observation,Observation对象保存了观
察者对象,接收到通知观察者所执行的方法,以及下一个Observation对象的地
址。
2. 根据是否传入NotificationName选择操作Named Table还是Nameless
Table。
3. 若传入了NotificationName,则会以NotificationName为key去查找对应的
Value,若找到value,则取出对应的value;若未找到对应的value,则新建一个
table,然后将这个table以NotificationName为key添加到Named Table中。
4. 若在保存Observation的table中,以object为key取对应的链表。若找到了则
直接在链接末尾插入之前实例化好的Observation;若未找到则以之前实例化好
的Observation对象作为头节点插入进去。
5. 如果没有通知名称:
直接根据object 去 NameLess表找
6. 如果没有名称,也没有object呢?
直接去链表尾部插入
发送通知流程
发送通知一般调用`postNotificationName:object:userInfo:`来实现,内部会
根据传入的参数实例化一个NSNotification对象,包含name、object、userinfo
等信息
发送通知的流程总体来说是根据NotificationName和object找到对应的链表,然
后遍历整个链表,给每个Observation节点中保存的oberver发送对应的SEL消息
1. 首先会创建一个数组observerArray用来保存需要通知的observer。
2. 遍历wildcard链表,将observer添加到observerArray数组中。
3. 若存在object,在nameless table中找到以object为key的链表,然后遍历找
到的链表,将observer添加到observerArray数组中。
4. 若存在NotificationName,在named table中以NotificationName为key找
到对应的table,然后再在找到的table中以object为key找到对应的链表,遍历链
表,将observer添加到observerArray数组中。如果object不为nil,则以nil为
key找到对应的链表,遍历链表,将observer添加到observerArray数组中。
5. 至此所有关于当前通知的observer(wildcard+nameless+named)都已经
加入到了数组observerArray中。遍历observerArray数组,取出其中的
observer节点(包含了观察者对象和selector),调用形式如下:
[o->observer performSelector: o->selector withObject: notification];
哪个线程发送,哪个线程接收:
发送通知的时候直接在当前线程去发送消息,因此, 发送通知的线程和接收通知
的线程是同一线程
发送通知是同步的
顺序遍历,同步
移除通知流程
1. 若NotificationName和object都为nil,则清空wildcard链表。
2. 若NotificationName为nil,遍历named table,若object为nil,则清空
named table,若object不为nil,则以object为key找到对应的链表,然后清空
链表。在nameless table中以object为key找到对应的observer链表,然后清
空,若object也为nil,则清空nameless table。
3. 若NotificationName不为nil,在named table中以NotificationName为key
找到对应的table,若object为nil,则清空找到的table,若object不为nil,则以
object为key在找到的table中取出对应的链表,然后清空链表。
cache 哈希表
用于快速缓存
NSNotificationQueue
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
NSPostWhenIdle = 1, // 当runloop处于空闲状态时post
NSPostASAP = 2, // 当当前runloop完成之后立即post
NSPostNow = 3 // 立即post
};
NSNotification *notification = [NSNotification
notificationWithName:@"111" object:nil];
[[NSNotificationQueue defaultQueue] enqueueNotification: notification
postingStyle:NSPostASAP];
- NSPostingStyle为NSPostNow 模式是同步发送,
- NSPostWhenIdle或者NSPostASAP是异步发送
NSNotificationQueue将通知添加到队列中时,其中postringStyle参数就是定义
通知调用和runloop状态之间关系。
指定主线程
使用block接口addObserverForName:object:queue:usingBlock:指定主线程
多次添加同一个通知
由于源码中并不会进行重复过滤,所以添加同一个通知,等于就是添加了2次,
回调也会触发两次
多次移除通知
多次移除,并没有问题,因为会去map中查找,找到才会删除。当name和
object都为nil时,会移除所有关于该observer的WILDCARD
子线程发送通知 crash 异常问题
你在子线程发通知,默认子线程执行,这时候如果主线程把收通知的self释放
了,子线程还未执行完的代码就会出问题 EXC_BAD_ACCESS
解决:
为了避免通知因为线程阻塞,最好是主线程发通知,需要耗时操作的逻辑在接收
到通知以后自己开线程处理。