NSNotification 解決的問(wèn)題
- 可以實(shí)現(xiàn)跨層的傳遞,例如A頁(yè)面跳轉(zhuǎn)到B頁(yè)面,B頁(yè)面再跳轉(zhuǎn)到C頁(yè)面,這時(shí)候如果我們通過(guò)委托回調(diào)的模式讓A知道C的一些修改馏段,那么實(shí)現(xiàn)起來(lái)就會(huì)很麻煩。
- 可以實(shí)現(xiàn)一對(duì)多践瓷,
NSNotification
的實(shí)際是一種觀察者模式。
NSNotificationCenter
NSNotificationCenter
就相當(dāng)于一個(gè)廣播站亡蓉,使用 [NSNotificationCenter defaultCenter]
來(lái)獲取晕翠,NSNotificationCenter
實(shí)際上是 iOS
程序內(nèi)部之間的一種消息廣播機(jī)制,主要為了解決應(yīng)用程序內(nèi)部不同對(duì)象之間解耦而設(shè)計(jì)。
NSNotificationCenter
是整個(gè)通知機(jī)制的關(guān)鍵所在淋肾,它管理著監(jiān)聽(tīng)者的注冊(cè)和注銷硫麻,通知的發(fā)送和接收。NSNotificationCenter
維護(hù)著一個(gè)通知的分發(fā)表樊卓,把所有通知發(fā)送者發(fā)送的通知拿愧,轉(zhuǎn)發(fā)給對(duì)應(yīng)的監(jiān)聽(tīng)者們。每一個(gè) iOS
程序都有一個(gè)唯一的通知中心碌尔,不必自己去創(chuàng)建一個(gè)浇辜,它是一個(gè)單例,通過(guò) [NSNotificationCenter defaultCenter]
方法獲取唾戚。
NSNotificationCenter
是基于觀察者模式設(shè)計(jì)的柳洋,不能跨應(yīng)用程序進(jìn)程通信,當(dāng) NSNotificationCenter
接收到消息之后會(huì)根據(jù)內(nèi)部的消息轉(zhuǎn)發(fā)表叹坦,將消息發(fā)送給訂閱者熊镣;它可以向應(yīng)用任何地方發(fā)送和接收通知。
在 NSNotificationCenter
注冊(cè)觀察者募书,發(fā)送者使用通知中心廣播時(shí)绪囱,以 NSNotification
的 name
和 object
來(lái)確定需要發(fā)送給哪個(gè)觀察者。為保證觀察者能接收到通知莹捡,所以應(yīng)先向通知中心注冊(cè)觀察者鬼吵,接著再發(fā)送通知這樣才能在通知中心調(diào)度表中查找到相應(yīng)觀察者進(jìn)行通知。
NSNotification
NSNotification
是 NSNotificationCenter
接收到消息之后根據(jù)內(nèi)部的消息轉(zhuǎn)發(fā)表道盏,將消息發(fā)送給訂閱者封裝的對(duì)象而柑;
@interface NSNotification : NSObject <NSCopying, NSCoding>
//這個(gè)成員變量是這個(gè)消息對(duì)象的唯一標(biāo)識(shí),用于辨別消息對(duì)象
@property (readonly, copy) NSString *name;
// 這個(gè)成員變量定義一個(gè)對(duì)象荷逞,可以理解為針對(duì)某一個(gè)對(duì)象的消息媒咳,代表通知的發(fā)送者
@property (nullable, readonly, retain) id object;
//這個(gè)成員變量是一個(gè)字典,可以用其來(lái)進(jìn)行傳值
@property (nullable, readonly, copy) NSDictionary *userInfo;
// 初始化方法
- (instancetype)initWithName:(NSString *)name object:(nullable id)object userInfo:(nullable NSDictionary *)userInfo NS_AVAILABLE(10_6, 4_0) NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
@end
由于 NSNotification
屬性都是只讀的种远,如果要?jiǎng)?chuàng)建通知?jiǎng)t要用下面 NSNotification(NSNotificationCreation)
分類相應(yīng)的方法進(jìn)行初始化涩澡;
NSNotification
不能通過(guò) init
實(shí)例化,這樣會(huì)引起下面的異常坠敷,比如:
NSNotification *notif = [[NSNotification alloc] init];
*** Terminating app due to uncaught exception 'NSGenericException', reason: '*** -[NSConcreteNotification init]: should never be used'
@interface NSNotification (NSNotificationCreation)
+ (instancetype)notificationWithName:(NSString *)aName object:(nullable id)anObject;
+ (instancetype)notificationWithName:(NSString *)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
- (instancetype)init /*NS_UNAVAILABLE*/; /* do not invoke; not a valid initializer for this class */
@end
注意:
如果 NSNotification
對(duì)象中的 notificationName
為 nil
妙同,則會(huì)接收所有的通知。通知中心是以 NSNotification
的 name
和 object
來(lái)確定需要發(fā)送給哪個(gè)觀察者膝迎。監(jiān)聽(tīng)同一條通知的多個(gè)觀察者粥帚,在通知到達(dá)時(shí),它們執(zhí)行回調(diào)的順序是不確定的限次,所以我們不能去假設(shè)操作的執(zhí)行會(huì)按照添加觀察者的順序來(lái)執(zhí)行芒涡。
通知中心默認(rèn)是以同步的方式發(fā)送通知的柴灯,也就是說(shuō),當(dāng)一個(gè)對(duì)象發(fā)送了一個(gè)通知费尽,只有當(dāng)該通知的所有接受者都接受到了通知中心分發(fā)的通知消息并且處理完成后赠群,發(fā)送通知的對(duì)象才能繼續(xù)執(zhí)行接下來(lái)的方法。
NSNotificationQueue
NSNotificationQueue
通知隊(duì)列旱幼,用來(lái)管理多個(gè)通知的調(diào)用查描。通知隊(duì)列通常以先進(jìn)先出 FIFO
順序維護(hù)通。NSNotificationQueue
就像一個(gè)緩沖池把一個(gè)個(gè)通知放進(jìn)池子中柏卤,使用特定方式通過(guò) NSNotificationCenter
發(fā)送到相應(yīng)的觀察者冬三。下面我們會(huì)提到特定的方式即合并通知和異步通知。
創(chuàng)建通知隊(duì)列方法:
- (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter NS_DESIGNATED_INITIALIZER;
往隊(duì)列加入通知方法:
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSNotificationCoalescing)coalesceMask forModes:(nullable NSArray<NSRunLoopMode> *)modes;
移除隊(duì)列中的通知方法:
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;
發(fā)送方式
NSPostingStyle包括三種類型:
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
NSPostWhenIdle = 1,
NSPostASAP = 2,
NSPostNow = 3
};
NSPostWhenIdle
:空閑發(fā)送通知闷旧,當(dāng)運(yùn)行循環(huán)處于等待或空閑狀態(tài)時(shí)长豁,發(fā)送通知,對(duì)于不重要的通知可以使用忙灼。
NSPostASAP
:盡快發(fā)送通知匠襟,當(dāng)前運(yùn)行循環(huán)迭代完成時(shí),通知將會(huì)被發(fā)送该园,有點(diǎn)類似沒(méi)有延遲的定時(shí)器酸舍。
NSPostNow
:同步發(fā)送通知,如果不使用合并通知和 postNotification:
一樣是同步通知里初。
合并通知
NSNotificationCoalescing也包括三種類型:
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
NSNotificationNoCoalescing = 0,
NSNotificationCoalescingOnName = 1,
NSNotificationCoalescingOnSender = 2
};
NSNotificationNoCoalescing
:不合并通知啃勉。
NSNotificationCoalescingOnName
:合并相同名稱的通知。
NSNotificationCoalescingOnSender
:合并相同通知和同一對(duì)象的通知双妨。
通過(guò)合并我們可以用來(lái)保證相同的通知只被發(fā)送一次淮阐。forModes:(nullable NSArray<NSRunLoopMode> *)modes
可以使用不同的 NSRunLoopMode
配合來(lái)發(fā)送通知,可以看出實(shí)際上 NSNotificationQueue
與 RunLoop
的機(jī)制以及運(yùn)行循環(huán)有關(guān)系刁品,通過(guò) NSNotificationQueue
隊(duì)列來(lái)發(fā)送的通知和關(guān)聯(lián)的 RunLoop
運(yùn)行機(jī)制來(lái)進(jìn)行的泣特。
iOS 9 NSNotificationCenter 無(wú)需手動(dòng)移除觀察者
眾所周知,在觀察者對(duì)象釋放之前挑随,需要調(diào)用 removeObserver
方法状您,將觀察者從通知中心移除,否則程序可能會(huì)出現(xiàn)崩潰兜挨。其實(shí)膏孟,從 iOS 9
開(kāi)始,即使不移除觀察者對(duì)象拌汇,程序也不會(huì)出現(xiàn)異常柒桑。這是為什么呢?我們先了解一下噪舀,為什么 iOS 9
之前需要手動(dòng)移除觀察者對(duì)象幕垦。
在 MRC
時(shí)代丢氢,觀察者注冊(cè)時(shí)傅联,通知中心并不會(huì)對(duì)觀察者對(duì)象做 retain
操作先改,而是對(duì)觀察者對(duì)象進(jìn)行 unsafe_unretained
引用。
// for attribute
@property (unsafe_unretained) NSObject *unsafeProperty;
// for variables
NSObject *__unsafe_unretained unsafeReference;
不安全引用(unsafe reference
)和弱引用 (weak reference
) 類似蒸走,它并不會(huì)讓被引用的對(duì)象保持存活仇奶,但是和弱引用不同的是,當(dāng)被引用的對(duì)象釋放的時(shí)比驻,不安全引用并不會(huì)自動(dòng)被置為 nil
该溯,這就意味著它變成了野指針,而對(duì)野指針發(fā)送消息會(huì)導(dǎo)致程序崩潰别惦。
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.
而在 iOS 9
以后狈茉,通知中心持有的是注冊(cè)者的 weak
指針,這時(shí)即使不對(duì)通知進(jìn)行手動(dòng)移除掸掸,指針也會(huì)在注冊(cè)者被回收后自動(dòng)置空氯庆。但是,通過(guò) -[NSNotificationCenter addObserverForName:object:queue:usingBlock]
方法注冊(cè)的觀察者依然需要手動(dòng)的釋放扰付,因?yàn)橥ㄖ行膶?duì)它們持有的是強(qiáng)引用堤撵。
NSNotification在多線程中使用
在多線程中,無(wú)論在哪個(gè)線程注冊(cè)了觀察者羽莺,Notification
接收和處理都是在發(fā)送 Notification
的線程中的实昨。所以,當(dāng)我們需要在接收到 Notification
后作出更新 UI
操作的話盐固,就需要考慮線程的問(wèn)題了荒给,如果在子線程中發(fā)送 Notification
,想要在接收到 Notification
后更新 UI
的話就要切換回到主線程刁卜。
- (void)viewDidLoad {
[super viewDidLoad];
NSString *NOTIFICATION_NAME = @"NOTIFICATION_NAME";
NSLog(@"Current thread = %@", [NSThread currentThread]);
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:NOTIFICATION_NAME object:nil];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"Post notification志电,Current thread = %@", [NSThread currentThread]);
[[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_NAME object:nil userInfo:nil];
});
}
- (void)handleNotification:(NSNotification *)notification {
NSLog(@"Receive notification,Current thread = %@", [NSThread currentThread]);
}
運(yùn)行結(jié)果:
2017-03-11 17:56:33.898 NotificationTest[23457:1615587] Current thread = <NSThread: 0x608000078080>{number = 1, name = main}
2017-03-11 17:56:33.899 NotificationTest[23457:1615738] Post notification长酗,Current thread = <NSThread: 0x60000026c500>{number = 3, name = (null)}
2017-03-11 17:56:33.899 NotificationTest[23457:1615738] Receive notification溪北,Current thread = <NSThread: 0x60000026c500>{number = 3, name = (null)}
上面我們?cè)谥骶€程注冊(cè)觀察者,在子線程發(fā)送 Notification
夺脾,最后 Notification
的接收和處理也是在子線程之拨。
注意:
在一個(gè)多線程的程序中,發(fā)送方發(fā)送通知的線程通常就是監(jiān)聽(tīng)者接受通知的線程咧叭,這可能和監(jiān)聽(tīng)者注冊(cè)時(shí)的線程不一樣蚀乔。
解決方法
MachPort的使用方式
最好的方法是在 Notification
所在的默認(rèn)線程中捕獲發(fā)送的通知,然后將其重定向到指定的線程中菲茬。關(guān)于 Notification
的重定向官方文檔給出了一個(gè)方法:
一種重定向的實(shí)現(xiàn)思路是自定義一個(gè)通知隊(duì)列(不是
NSNotificationQueue
對(duì)象)吉挣,讓這個(gè)隊(duì)列去維護(hù)那些我們需要重定向的Notification
派撕。我們?nèi)匀皇窍裰耙粯尤プ?cè)一個(gè)通知的觀察者,當(dāng)Notification
到達(dá)時(shí)睬魂,先看看post
這個(gè)Notification
的線程是不是我們所期望的線程终吼,如果不是,就將這個(gè)Notification
放到我們的隊(duì)列中氯哮,然后發(fā)送一個(gè)信號(hào)signal
到期望的線程中际跪,來(lái)告訴這個(gè)線程需要處理一個(gè)Notification
。指定的線程收到這個(gè)信號(hào)signal
后喉钢,將Notification
從隊(duì)列中移除姆打,并進(jìn)行后續(xù)處理。
// ViewController.m
// NotificationTest
//
// Created by sunjinshuai on 2017/3/11.
// Copyright ? 2017年 sunjinshuai. All rights reserved.
//
#import "ViewController.h"
@interface ViewController ()<NSMachPortDelegate>
@property (nonatomic) NSMutableArray *notifications; // 通知隊(duì)列
@property (nonatomic) NSThread *notificationThread; // 想要處理通知的線程(目標(biāo)線程)
@property (nonatomic) NSLock *notificationLock; // 用于對(duì)通知隊(duì)列加鎖的鎖對(duì)象肠虽,避免線程沖突
@property (nonatomic) NSMachPort *notificationPort; // 用于向目標(biāo)線程發(fā)送信號(hào)的通信端口
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString *NOTIFICATION_NAME = @"NOTIFICATION_NAME";
NSLog(@"Current thread = %@", [NSThread currentThread]);
[self setUpThreadingSupport];
// 注冊(cè)觀察者
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:NOTIFICATION_NAME object:nil];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 發(fā)送Notification
NSLog(@"Post notification幔戏,Current thread = %@", [NSThread currentThread]);
[[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_NAME object:nil userInfo:nil];
});
}
/*
在注冊(cè)任何通知之前,需要先初始化屬性税课。下面方法初始化了隊(duì)列和鎖定對(duì)象闲延,保留對(duì)當(dāng)前線程對(duì)象的引用,并創(chuàng)建一個(gè)Mach通信端口伯复,將其添加到當(dāng)前線程的運(yùn)行循環(huán)中慨代。
此方法運(yùn)行后,發(fā)送到notificationPort的任何消息都會(huì)在首次運(yùn)行此方法的線程的run loop中接收啸如。如果接收線程的run loop在Mach消息到達(dá)時(shí)沒(méi)有運(yùn)行侍匙,則內(nèi)核保持該消息,直到下一次進(jìn)入run loop叮雳。接收線程的run loop將傳入消息發(fā)送到端口delegate的handleMachMessage:方法想暗。
*/
- (void)setUpThreadingSupport {
if (self.notifications) {
return;
}
self.notifications = [[NSMutableArray alloc] init];
self.notificationLock = [[NSLock alloc] init];
self.notificationThread = [NSThread currentThread];
self.notificationPort = [[NSMachPort alloc] init];
[self.notificationPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:self.notificationPort
forMode:(__bridge NSString*)kCFRunLoopCommonModes];
}
/**
端口的代理方法
*/
- (void)handleMachMessage:(void *)msg {
[self.notificationLock lock];
while ([self.notifications count]) {
NSNotification *notification = [self.notifications objectAtIndex:0];
[self.notifications removeObjectAtIndex:0];
[self.notificationLock unlock];
[self processNotification:notification];
[self.notificationLock lock];
};
[self.notificationLock unlock];
}
- (void)processNotification:(NSNotification *)notification {
//判斷是不是目標(biāo)線程,不是則轉(zhuǎn)發(fā)到目標(biāo)線程
if ([NSThread currentThread] != _notificationThread) {
// 將Notification轉(zhuǎn)發(fā)到目標(biāo)線程
[self.notificationLock lock];
[self.notifications addObject:notification];
[self.notificationLock unlock];
[self.notificationPort sendBeforeDate:[NSDate date]
components:nil
from:nil
reserved:0];
} else {
// 在此處理通知
NSLog(@"Receive notification帘不,Current thread = %@", [NSThread currentThread]);
NSLog(@"Process notification");
}
}
@end
打印結(jié)果:
2017-03-11 18:28:55.788 NotificationTest[24080:1665269] Current thread = <NSThread: 0x60800006d4c0>{number = 1, name = main}
2017-03-11 18:28:55.789 NotificationTest[24080:1665396] Post notification说莫,Current thread = <NSThread: 0x60800026bc40>{number = 4, name = (null)}
2017-03-11 18:28:55.795 NotificationTest[24080:1665269] Receive notification,Current thread = <NSThread: 0x60800006d4c0>{number = 1, name = main}
2017-03-11 18:28:55.795 NotificationTest[24080:1665269] Process notification
在發(fā)送通知的子線程處理通知的事件時(shí)寞焙,將 NSNotification
暫存储狭,然后通過(guò) MachPort
往相應(yīng)線程的 RunLoop
中發(fā)送事件。相應(yīng)的線程收到該事件后捣郊,取出在隊(duì)列中暫存的 NSNotification
, 然后在當(dāng)前線程中調(diào)用處理通知的方法辽狈。
可以看到,運(yùn)行結(jié)果結(jié)果我們想要的:在子線程中發(fā)送 Notification
呛牲,在主線程中接收與處理 Notification
刮萌。
上面的實(shí)現(xiàn)方法也不是絕對(duì)完美的,蘋果官方指出了這種方法的限制:
- 所有線程的
Notification
的處理都必須通過(guò)相同的方法processNotification:
娘扩。 - 每個(gè)對(duì)象必須提供自己的實(shí)現(xiàn)和通信端口着茸。
block
上面蘋果官方給我們提供的方法外壮锻,我們還可以利用基于 block
的 NSNotification
去實(shí)現(xiàn),apple
從 ios4
之后提供了帶有 block
的 NSNotification
涮阔。使用方式如下:
- (id<NSObject>)addObserverForName:(NSString *)name
object:(id)obj
queue:(NSOperationQueue *)queue
usingBlock:(void (^)(NSNotification *note))block
其中:
- 觀察者就是當(dāng)前對(duì)象
-
queue
定義了block
執(zhí)行的線程猜绣,nil
則表示block
的執(zhí)行線程和發(fā)通知在同一個(gè)線程 -
block
就是相應(yīng)通知的處理函數(shù)
這個(gè) API
已經(jīng)能夠讓我們方便的控制通知的線程切換。但是澎语,這里有個(gè)問(wèn)題需要注意途事。就是其 remove
操作。
原來(lái)的 NSNotification
的 remove
方式如下:
- (void)removeObservers {
[[NSNotificationCenter defaultCenter] removeObserver:self name:POST_NOTIFICATION object:nil];
}
但是帶 block
方式的 remove
便不能像上面這樣處理了擅羞。其方式如下:
- (void)removeObservers {
if(_observer){
[[NSNotificationCenter defaultCenter] removeObserver:_observer];
}
}
其中 _observer
是 addObserverForName
方式的 api
返回觀察者對(duì)象。這也就意味著义图,你需要為每一個(gè)觀察者記錄一個(gè)成員對(duì)象减俏,然后在 remove
的時(shí)候依次刪除。試想一下碱工,你如果需要 10 個(gè)觀察者娃承,則需要記錄 10 個(gè)成員對(duì)象,這個(gè)想想就是很麻煩怕篷,而且它還不能夠方便的指定 observer
历筝。因此,理想的做法就是自己再做一層封裝廊谓,將這些細(xì)節(jié)封裝起來(lái)梳猪。
當(dāng)然,想要在子線程發(fā)送 Notification
蒸痹、接收到 Notification
后在主線程中做后續(xù)操作春弥,可以用一個(gè)很笨的方法,在 handleNotification
里面強(qiáng)制切換線程:
- (void)handleNotification:(NSNotification *)notification {
NSLog(@"Receive notification叠荠,Current thread = %@", [NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"Current thread = %@", [NSThread currentThread]);
});
}
在簡(jiǎn)單情況下可以使用這種方法匿沛,但是當(dāng)我們發(fā)送了多個(gè) Notification
并且有多個(gè)觀察者的時(shí)候,難道我們要在每個(gè)地方都手動(dòng)切換線程榛鼎?所以逃呼,這種方法并不是一個(gè)有效的方法。
通知的實(shí)現(xiàn)原理
以下源碼來(lái)自于libs-base
typedef struct NCTbl {
Observation *wildcard; /* Get ALL messages. */
GSIMapTable nameless; /* Get messages for any name. */
GSIMapTable named; /* Getting named messages only. */
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;
typedef struct Obs {
id observer; /* Object to receive message. */
SEL selector; /* Method selector. */
struct Obs *next; /* Next item in linked list. */
int retained; /* Retain count for structure. */
struct NCTbl *link; /* Pointer back to chunk table */
} Observation;
從源碼中可以看出者娱,在 NSNotificationCenter
內(nèi)部一共保存了兩張 MapTable
抡笼。一張用于保存添加觀察者的時(shí)候傳入了 NotifcationName
的情況;一張用于保存添加觀察者的時(shí)候沒(méi)有傳入了 NotifcationName
的情況肺然。
Example
https://github.com/iOS-Advanced/iOS-Advanced/tree/master/sourcecode/NSNotificationTheory