背景
iOS 開發(fā)中,用到通知中心的話梁肿,一般要在 dealloc 方法中主動(dòng)移除觀察者,否則有可能造成崩潰。在 iOS9 及以后系統(tǒng)炫彩,我們不需要 dealloc 方法中主動(dòng)移除觀察者,除非使用了 addObserverForName:object:queue:usingBlock:
方法監(jiān)聽通知絮短。官方文檔介紹如下:
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 通知中心現(xiàn)狀分析
在 iOS9 之前的系統(tǒng)江兢,通知中心對(duì)觀察者用 unsafe_unretained 修飾,而在 iOS9 及以后系統(tǒng)丁频,用 weak 修飾杉允。
unsafe_unretained 和 weak 的區(qū)別在于:
當(dāng) weak 指針?biāo)赶虻膶?duì)象被釋放時(shí),weak 指針會(huì)被自動(dòng)置為nil; 而 unsafe_unretain 指針指向的對(duì)象被釋放時(shí)席里,unsafe_unretain 指針不會(huì)被置為 nil ,而變成了野指針叔磷,再次使用就會(huì)造成 crash 。
值得注意的是奖磁,在 iOS9 及以后系統(tǒng)改基,如果使用了 addObserverForName:object:queue:usingBlock: 方法監(jiān)聽通知,我們依然需要在 dealloc 方法中主動(dòng)移除觀察者咖为。因?yàn)橥ㄖ行臅?huì)將 block copy 保存秕狰,不主動(dòng)移除的話,通知中心就能一直監(jiān)聽通知躁染。
- (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 監(jiān)聽通知的鸣哀,需要該方法的調(diào)用者持有觀察者,并在 dealloc 中吞彤,使用 removeObserver: 方法我衬,移除觀察者。
期望
使用者無(wú)需手動(dòng)移除觀察者备畦,即使使用 block 監(jiān)聽通知低飒。
方案
通知中心持有觀察者對(duì)象組成的數(shù)組,為了使觀察者被釋放后懂盐,指向觀察者的指針不會(huì)變成野指針褥赊,數(shù)組對(duì)數(shù)組里的觀察者需要是弱引用。能實(shí)現(xiàn)這一需求的有兩種方式:
- NSPointerArray
- NSMutableArray< [NSValue valueWithNonretainedObject:object] *>
NSPointerArray
特性:
- 與 NSMutableArray 相似莉恼,可以添加拌喉、移除對(duì)象速那,以及遍歷對(duì)象
-
+ (NSPointerArray *)weakObjectsPointerArray
返回一個(gè)對(duì)元素弱引用的 pointer 數(shù)組。 -
+ (NSPointerArray *)strongObjectsPointerArray
返回一個(gè)對(duì)元素強(qiáng)引用的 pointer 數(shù)組尿背。 - 可以添加 nil 對(duì)象端仰,且 count 屬性是 NSPointerArray 中所有元素的個(gè)數(shù),包含 nil 對(duì)象田藐。allObjects 屬性 是 NSPointerArray 中所有非 nil 對(duì)象組成的數(shù)組
- compact 方法荔烧,可以移除 NSPointerArray 中所有 nil 對(duì)象。注意:實(shí)際調(diào)用 compact 方法前汽久,需要先執(zhí)行 [obj addPointer:nil] 鹤竭,不加上這句的話,直接調(diào)用compact景醇,并不能清除 array 中的 nil 對(duì)象臀稚。
NSMutableArray< [NSValue valueWithNonretainedObject:object] *>
特性:
- 由 NSValue 對(duì)象組成的數(shù)組,NSValue 對(duì)象對(duì) object 弱引用三痰。
綜合對(duì) NSPointerArray 和 NSMutableArray< NSValue *> 特性的對(duì)比吧寺,我選擇使用提供有多個(gè)方便的 API 的 NSPointerArray 。
分析及實(shí)現(xiàn)
分析
通知中心是一個(gè)單例散劫,提供有添加監(jiān)聽方法稚机、發(fā)送通知方法、移除通知方法舷丹。為了保持與系統(tǒng)通知中心的使用習(xí)慣一致抒钱,我在自己的通知中心 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;
/** 鎖。防止線程競(jìng)爭(zhēng) */
@property (nonatomic, strong) NSLock *lock;
observerDict (觀察者字典) 以 通知名 為 key, value 是 觀察者 組成的 NSPointerArray , NSPointerArray 對(duì) 觀察者 是弱引用颜凯。
observerInfoDict (觀察信息字典) 以 通知名 為 key, value 是 觀察信息(CustomObserverInfo) 組成的 NSMutableSet , CustomObserverInfo 對(duì) 觀察者 是弱引用。
實(shí)現(xiàn)
1. 添加監(jiān)聽方法
偽代碼:
通知名 或 觀察者 不存在仗扬,return;
if ( 觀察者字典 中不存在以通知名為 key 的 NSPointerArray) {
觀察者字典 中添加以通知名為 key症概,以 弱引用的 NSPointerArray 空對(duì)象 為 value 的 key-value
}
if ( 觀察者字典 中以通知名為 key 的 NSPointerArray value 中,不存在 要添加的觀察者) {
觀察者字典 中以通知名為 key 的 NSPointerArray value 中早芭,添加該觀察者
}
if ( 觀察信息字典 中以通知名為 key 的 通知信息 集合中彼城,不存在 完全一致的通知信息) {
觀察信息字典 中添加以通知名為 key ,以該 通知信息 為首個(gè)元素的集合作為 value 的 key-value
}
2. 發(fā)送通知方法
偽代碼:
通知名 不存在退个,return;
if ( 觀察者字典 中不存在以通知名為 key 的 NSPointerArray) return;
if ( 觀察信息字典 中以通知名為 key 的 通知信息集合 value 不存在) return;
遍歷 觀察信息字典 中以通知名為 key 的 通知信息集合 value 中的 通知信息對(duì)象 info
if (info.observer == observer && info.object == anObject) { // 通知信息完全相同
if (info.aSelector) {
執(zhí)行 selector
} else if (info.block) {
執(zhí)行 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 {
// 因?yàn)?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. 收到內(nèi)存警告舱馅,刪除觀察者為 nil 的對(duì)象
收到內(nèi)存警告,則清空 觀察者字典 和 觀察信息字典 中刀荒,觀察者 為 nil 的 key-value 鍵值對(duì)
1. 使用 compact 方法代嗤,刪除 觀察者 NSPointerArray 中棘钞,觀察者為 nil 的對(duì)象
2. 刪掉觀察信息集合 NSMutableSet 中,觀察信息 CustomObserverInfo 的 觀察者 為 nil 的對(duì)象
注意點(diǎn)
- 通知中心要監(jiān)聽內(nèi)存警告干毅,接收到內(nèi)存警告時(shí)宜猜,可以清除掉觀察者為 nil 的對(duì)象,回收內(nèi)存硝逢。
- 對(duì) 觀察者字典 和 觀察信息字典 進(jìn)行存取時(shí)姨拥,需要加鎖,避免線程競(jìng)爭(zhēng)
- 使用 - removeObserver:name:object: 方法移除觀察者時(shí)渠鸽,只有當(dāng)觀察信息垫毙,包括 observer、name拱绑、object 均一致時(shí)综芥,才能將 觀察者字典 中的觀察者移除,否則猎拨,只移除觀察信息字典中相同的觀察信息對(duì)象
拓展閱讀
在 - removeObserver:name:object: 方法中膀藐,涉及到對(duì) NSMutableSet、NSMutableDictionary红省、NSPointerArray 一邊遍歷额各,一邊刪除元素的場(chǎng)景。對(duì)于這些場(chǎng)景下是否會(huì)發(fā)生崩潰吧恃,以及如何避免崩潰虾啦,可參考 EnumerateAndDelete
的結(jié)論。