自定义 iOS 通知中心实践

源码:YHNotificationCenter

背景

iOS 开发中,用到通知中心的话,一般要在 dealloc 方法中主动移除观察者,否则有可能造成崩溃。在 iOS9 及以后系统,我们不需要 dealloc 方法中主动移除观察者,除非使用了 addObserverForName:object:queue:usingBlock: 方法监听通知。官方文档介绍如下:

If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its dealloc method. Otherwise, you should call this method or removeObserver:name:object: before observer or any object specified in addObserverForName:object:queue:usingBlock: or addObserver:selector:name:object: is deallocated.

iOS 通知中心现状分析

在 iOS9 之前的系统,通知中心对观察者用 unsafe_unretained 修饰,而在 iOS9 及以后系统,用 weak 修饰。
unsafe_unretainedweak 的区别在于:
当 weak 指针所指向的对象被释放时,weak 指针会被自动置为nil; 而 unsafe_unretain 指针指向的对象被释放时,unsafe_unretain 指针不会被置为 nil ,而变成了野指针,再次使用就会造成 crash 。
值得注意的是,在 iOS9 及以后系统,如果使用了 addObserverForName:object:queue:usingBlock: 方法监听通知,我们依然需要在 dealloc 方法中主动移除观察者。因为通知中心会将 block copy 保存,不主动移除的话,通知中心就能一直监听通知。

- (id<NSObject>)addObserverForName:(NSNotificationName)name object:(id)obj queue:(NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block;

block : The block is copied by the notification center and (the copy) held until the observer registration is removed.
The return value is retained by the system, and should be held onto by the caller in order to remove the observer with removeObserver: later, to stop observation.
使用 block 监听通知的,需要该方法的调用者持有观察者,并在 dealloc 中,使用 removeObserver: 方法,移除观察者。

期望

使用者无需手动移除观察者,即使使用 block 监听通知。

方案

通知中心持有观察者对象组成的数组,为了使观察者被释放后,指向观察者的指针不会变成野指针,数组对数组里的观察者需要是弱引用。能实现这一需求的有两种方式:

  1. NSPointerArray
  2. NSMutableArray< [NSValue valueWithNonretainedObject:object] *>

NSPointerArray

特性:

  1. 与 NSMutableArray 相似,可以添加、移除对象,以及遍历对象
  2. + (NSPointerArray *)weakObjectsPointerArray 返回一个对元素弱引用的 pointer 数组。
  3. + (NSPointerArray *)strongObjectsPointerArray 返回一个对元素强引用的 pointer 数组。
  4. 可以添加 nil 对象,且 count 属性是 NSPointerArray 中所有元素的个数,包含 nil 对象。allObjects 属性 是 NSPointerArray 中所有非 nil 对象组成的数组
  5. compact 方法,可以移除 NSPointerArray 中所有 nil 对象。注意:实际调用 compact 方法前,需要先执行 [obj addPointer:nil] ,不加上这句的话,直接调用compact,并不能清除 array 中的 nil 对象。

NSMutableArray< [NSValue valueWithNonretainedObject:object] *>

特性:

  1. 由 NSValue 对象组成的数组,NSValue 对象对 object 弱引用。
综合对 NSPointerArray 和 NSMutableArray< NSValue *> 特性的对比,我选择使用提供有多个方便的 API 的 NSPointerArray 。

分析及实现

分析

通知中心是一个单例,提供有添加监听方法、发送通知方法、移除通知方法。为了保持与系统通知中心的使用习惯一致,我在自己的通知中心 CustomNotificationCenter.h 文件中提供了下面这些方法:

+ (instancetype)defaultCenter;

- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;

- (void)addObserverForName:(nullable NSNotificationName)name observer:(nullable id)observer queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(CustomObserverInfo *info))block;

- (void)postNotification:(NSNotification *)notification;

- (void)postNotificationName:(NSNotificationName)aName;

- (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;

在通知中心 CustomNotificationCenter.m 文件中提供了下面这些属性:

/** 观察者字典 */
@property (nonatomic, strong) NSMutableDictionary<NSNotificationName, NSPointerArray *> *observerDict;

/** 观察信息字典 */
@property (nonatomic, strong) NSMutableDictionary<NSNotificationName, NSMutableSet<CustomObserverInfo *> *> *observerInfoDict;

/** 锁。防止线程竞争 */
@property (nonatomic, strong) NSLock *lock;

observerDict (观察者字典) 以 通知名 为 key, value 是 观察者 组成的 NSPointerArray , NSPointerArray 对 观察者 是弱引用。
observerInfoDict (观察信息字典) 以 通知名 为 key, value 是 观察信息(CustomObserverInfo) 组成的 NSMutableSet , CustomObserverInfo 对 观察者 是弱引用。

实现

1. 添加监听方法

伪代码:
 通知名 或 观察者 不存在,return;
 if ( 观察者字典 中不存在以通知名为 key 的 NSPointerArray) {
     观察者字典 中添加以通知名为 key,以 弱引用的 NSPointerArray 空对象 为 value 的 key-value
 }
 if ( 观察者字典 中以通知名为 key 的 NSPointerArray value 中,不存在 要添加的观察者) {
     观察者字典 中以通知名为 key 的 NSPointerArray value 中,添加该观察者
 }
 if ( 观察信息字典 中以通知名为 key 的 通知信息 集合中,不存在 完全一致的通知信息) {
     观察信息字典 中添加以通知名为 key ,以该 通知信息 为首个元素的集合作为 value 的 key-value
 }

2. 发送通知方法

伪代码:
 通知名 不存在,return;
 if ( 观察者字典 中不存在以通知名为 key 的 NSPointerArray) return;
 if ( 观察信息字典 中以通知名为 key 的 通知信息集合 value 不存在) return;
 遍历 观察信息字典 中以通知名为 key 的 通知信息集合 value 中的 通知信息对象 info
    if (info.observer == observer && info.object == anObject) { // 通知信息完全相同
         if (info.aSelector) {
             执行 selector
         } else if (info.block) {
             执行 block
         }
     } else { // 通知信息不完全相同
         return;
     }

3. 移除观察者方法

伪代码:
 if (observer == nil ) return;
 if (aName != nil) {
     在 observerDict 中 以 aName 为 key 的 NSPointerArray
         if (NSPointerArray 中 有 pointer 和 observer 相同) {
             在 observerInfoDict 中查找 aName 和 observer 都相同的 CustomObserverInfo
                 if (CustomObserverInfo.object == anObject) {
                     将 CustomObserverInfo 从 observerInfoDict 中移除
                 } else {
                     // 因为 CustomObserverInfo.object != anObject,所以 observerDict 不能移除 pointer
                 }
                 if (所以的 CustomObserverInfo.object == anObject) {
                     将 pointer 从 observerDict 中移除
                 }
         }
 } else if (!aName && anObject) {
     在 observerInfoDict 中遍历 CustomObserverInfo
         if (CustomObserverInfo.observer == observer && CustomObserverInfo.object == anObject) {
             将 CustomObserverInfo 从 observerInfoDict 中移除
         }
 } else if (!aName && !anObject) {
     移除 observerDict.NSPointerArray 中,所有 pointer == observer 的 pointer
     移除 observerInfoDict.NSMutableSet<CustomObserverInfo *> 中,所有 CustomObserverInfo.observer == observer 的 CustomObserverInfo.observer
 }

4. 收到内存警告,删除观察者为 nil 的对象

收到内存警告,则清空 观察者字典 和 观察信息字典 中,观察者 为 nil 的 key-value 键值对 
1. 使用 compact 方法,删除 观察者 NSPointerArray 中,观察者为 nil 的对象
2. 删掉观察信息集合 NSMutableSet 中,观察信息 CustomObserverInfo 的 观察者 为 nil 的对象

注意点

  1. 通知中心要监听内存警告,接收到内存警告时,可以清除掉观察者为 nil 的对象,回收内存。
  2. 对 观察者字典 和 观察信息字典 进行存取时,需要加锁,避免线程竞争
  3. 使用 - removeObserver:name:object: 方法移除观察者时,只有当观察信息,包括 observer、name、object 均一致时,才能将 观察者字典 中的观察者移除,否则,只移除观察信息字典中相同的观察信息对象

拓展阅读

在 - removeObserver:name:object: 方法中,涉及到对 NSMutableSet、NSMutableDictionary、NSPointerArray 一边遍历,一边删除元素的场景。对于这些场景下是否会发生崩溃,以及如何避免崩溃,可参考 EnumerateAndDelete
的结论。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容