背景
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_unretained 和 weak 的区别在于:
当 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 监听通知。
方案
通知中心持有观察者对象组成的数组,为了使观察者被释放后,指向观察者的指针不会变成野指针,数组对数组里的观察者需要是弱引用。能实现这一需求的有两种方式:
- NSPointerArray
- NSMutableArray< [NSValue valueWithNonretainedObject:object] *>
NSPointerArray
特性:
- 与 NSMutableArray 相似,可以添加、移除对象,以及遍历对象
-
+ (NSPointerArray *)weakObjectsPointerArray
返回一个对元素弱引用的 pointer 数组。 -
+ (NSPointerArray *)strongObjectsPointerArray
返回一个对元素强引用的 pointer 数组。 - 可以添加 nil 对象,且 count 属性是 NSPointerArray 中所有元素的个数,包含 nil 对象。allObjects 属性 是 NSPointerArray 中所有非 nil 对象组成的数组
- compact 方法,可以移除 NSPointerArray 中所有 nil 对象。注意:实际调用 compact 方法前,需要先执行 [obj addPointer:nil] ,不加上这句的话,直接调用compact,并不能清除 array 中的 nil 对象。
NSMutableArray< [NSValue valueWithNonretainedObject:object] *>
特性:
- 由 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 的对象
注意点
- 通知中心要监听内存警告,接收到内存警告时,可以清除掉观察者为 nil 的对象,回收内存。
- 对 观察者字典 和 观察信息字典 进行存取时,需要加锁,避免线程竞争
- 使用 - removeObserver:name:object: 方法移除观察者时,只有当观察信息,包括 observer、name、object 均一致时,才能将 观察者字典 中的观察者移除,否则,只移除观察信息字典中相同的观察信息对象
拓展阅读
在 - removeObserver:name:object: 方法中,涉及到对 NSMutableSet、NSMutableDictionary、NSPointerArray 一边遍历,一边删除元素的场景。对于这些场景下是否会发生崩溃,以及如何避免崩溃,可参考 EnumerateAndDelete
的结论。