自定義 iOS 通知中心實(shí)踐

源碼:YHNotificationCenter

背景

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_unretainedweak 的區(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)這一需求的有兩種方式:

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

NSPointerArray

特性:

  1. 與 NSMutableArray 相似莉恼,可以添加拌喉、移除對(duì)象速那,以及遍歷對(duì)象
  2. + (NSPointerArray *)weakObjectsPointerArray 返回一個(gè)對(duì)元素弱引用的 pointer 數(shù)組。
  3. + (NSPointerArray *)strongObjectsPointerArray 返回一個(gè)對(duì)元素強(qiáng)引用的 pointer 數(shù)組尿背。
  4. 可以添加 nil 對(duì)象端仰,且 count 屬性是 NSPointerArray 中所有元素的個(gè)數(shù),包含 nil 對(duì)象田藐。allObjects 屬性 是 NSPointerArray 中所有非 nil 對(duì)象組成的數(shù)組
  5. compact 方法荔烧,可以移除 NSPointerArray 中所有 nil 對(duì)象。注意:實(shí)際調(diào)用 compact 方法前汽久,需要先執(zhí)行 [obj addPointer:nil] 鹤竭,不加上這句的話,直接調(diào)用compact景醇,并不能清除 array 中的 nil 對(duì)象臀稚。

NSMutableArray< [NSValue valueWithNonretainedObject:object] *>

特性:

  1. 由 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)

  1. 通知中心要監(jiān)聽內(nèi)存警告干毅,接收到內(nèi)存警告時(shí)宜猜,可以清除掉觀察者為 nil 的對(duì)象,回收內(nèi)存硝逢。
  2. 對(duì) 觀察者字典 和 觀察信息字典 進(jìn)行存取時(shí)姨拥,需要加鎖,避免線程競(jìng)爭(zhēng)
  3. 使用 - 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é)論。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末痕寓,一起剝皮案震驚了整個(gè)濱河市傲醉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌呻率,老刑警劉巖硬毕,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異礼仗,居然都是意外死亡吐咳,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門元践,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)韭脊,“玉大人,你說(shuō)我怎么就攤上這事单旁』Ω幔” “怎么了?”我有些...
    開封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵慎恒,是天一觀的道長(zhǎng)任内。 經(jīng)常有香客問(wèn)我撵渡,道長(zhǎng),這世上最難降的妖魔是什么死嗦? 我笑而不...
    開封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任趋距,我火速辦了婚禮,結(jié)果婚禮上越除,老公的妹妹穿的比我還像新娘节腐。我一直安慰自己,他們只是感情好摘盆,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開白布翼雀。 她就那樣靜靜地躺著,像睡著了一般孩擂。 火紅的嫁衣襯著肌膚如雪狼渊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天类垦,我揣著相機(jī)與錄音狈邑,去河邊找鬼。 笑死蚤认,一個(gè)胖子當(dāng)著我的面吹牛米苹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播砰琢,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蘸嘶,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了陪汽?” 一聲冷哼從身側(cè)響起训唱,我...
    開封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎掩缓,沒想到半個(gè)月后雪情,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡你辣,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了尘执。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片舍哄。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖誊锭,靈堂內(nèi)的尸體忽然破棺而出表悬,到底是詐尸還是另有隱情,我是刑警寧澤丧靡,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布蟆沫,位于F島的核電站籽暇,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏饭庞。R本人自食惡果不足惜戒悠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望舟山。 院中可真熱鬧绸狐,春花似錦、人聲如沸累盗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)若债。三九已至符相,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蠢琳,已是汗流浹背啊终。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留挪凑,地道東北人孕索。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像躏碳,于是被迫代替她去往敵國(guó)和親搞旭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容