NSNotification在平時開發中使用非常頻繁。網上關于NSNotification介紹大多是停留在用法的介紹,基本上沒有深入介紹NSNotification原理的文章。故有此文!
通知基礎
NSNotification 是iOS中一個調度消息通知的類,采用單例模式設計,在程序中實現傳值、回調等地方應用很廣。在iOS中,NSNotification是使用觀察者模式來實現的用于跨層傳遞消息。往往也用NSNotification來實現解耦的目的。
通知這種傳值方式一般用于一對多的情況,iOS中常見的還有代理傳值、block傳值等。代理實現和block一般用于一對一的情況。至于具體區別接不贅述了。
總結一下筆者在項目中使用通知的過程中,平時注意的幾點:
- 通知的定義最好統一放在一個頭文件中定義好,命名也盡量規范,比如用APP名模塊名通知名這種方式,便于區分該通知具體實現什么目的。
- 全局最好維護一個單例來進行通知的發送。并且建立一張通知發送對象的表及接收通知對象表。因為在比較大的項目中,通知使用很頻繁的情況下,很難找到對應的位置。往往給開發埋下了嚴重的坑。
- 接收通知的線程,和發送通知所處的線程是同一個線程。也就是說如果如果要在接收通知的時候更新UI,需要注意發送通知的線程是否為主線程。
通知中的數據結構
在介紹原理之前,先弄清通知中的常見數據結構有助于深刻的理解整個過程。這也是筆者分析源碼常用方式。
NSNotification
NSNotification包含了一些用于向其他對象發送通知的必要信息,發送通知通過NSNotificationCenter發送,其中NSNotification主要的字段有如下幾個,也是發送通知必要的,注意NSNotification是一個不可變的對象。
字段名 | 含義 |
---|---|
name | 通知的名稱,用于通知的唯一標識 |
object | 保存發送通知的對象 |
userinfo | 保存給通知接受者傳遞的額外信息 |
可以使用notificationWithName:object:
或者notificationWithName:object:userInfo:
創建通知對象,但是一般情況下不會這樣直接創建。實際工作中更多是直接使用NSNotificationCenter調用postNotificationName:object:
或者 postNotificationName:object:userInfo:
方法發出通知,這兩個方法會在內部直接創建這個對象。
NSNotification是一個類簇,不能通過init實例化,比如NSNotification *notif = [[NSNotification alloc]init];
這樣會引起下面的異常。
但是可以通過裝飾構造方法創建實例對象,裝飾構造方法如下。:
initWithName:(NSNotificationName)name object:(nullable id)object userInfo:(nullable NSDictionary *)userInfo API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)) NS_DESIGNATED_INITIALIZER;
如果想要附加更多信息在NSNotification中,可以子類化NSNotification,額外新加的字段。需要注意的一點就是雖然可以自己去實現裝飾構造方法,但是切記在自定義的裝飾構造方法中不要調用[super init]。
NSNotificationCenter
NSNotificationCenter提供了一套機制來發送通知,本質上來講NSNotificationCenter其實就是一個通知派發表。至于為什么這么講,后面在介紹發送流程的時候會詳細介紹。
NSNotificationCenter暴露給外部的字段不多就只有一個defaultCenter,而且這個字段還是只讀的,其余的全是對通知操作的函數。
暴露出來的方法也就三種。前兩種是對觀察者的管理,第三種是用于發送通知。
作用 | 相關方法 |
---|---|
添加通知觀察者 | addObserverForName:object:queue:usingBlock: |
addObserver:selector:name:object:
|
| 移除通知觀察者 | removeObserver:
emoveObserver:name:object:
|
| 發出通知 | postNotification:
postNotificationName:object:
postNotificationName:object:userInfo: |
這里有下面幾點需要說明:
- 參數object表示的是觀察者只會接受來至object對象發出的所注冊的通知。而不會接受其他對象發送的所注冊的通知。
- 方法
addObserverForName:object:queue:usingBlock:
。因為平時這個用得不是特別多。相比addObserver:selector:name:object:
這種方式添加通知,多了個queue和block。這里的queue就是決定將block提交到那個隊列里面執行。通知接受是和發送通知的線程是同一個。常見的會把這個queue設置為主隊列,因為主隊列的任務都會在主線程下完成,所以可以用這種方式來實現通知更新UI。而不使用注冊SEL的方式。
如果你還不清楚隊列與線程的區別,建議自己親手去實踐一遍。可以簡單說下主隊列,主隊列(串行隊列)中的任務都是在主線程中完成,無論是同步還是異步執行。
NSNotificationQueue
NSNotificationQueue在NSNotificationCenter起到了一個緩沖的作用。盡管NSNotificationCenter已經分發通知,但放入隊列的通知可能會延遲,直到當前的runloop結束或runloop處于空閑狀態才發送。具體策略是由后面的參數決定。
如果有多個相同的通知,可以在NSNotificationQueue進行合并,這樣只會發送一個通知。NSNotificationQueue會通過先進先出的方式來維護NSNotification的實例,當通知實例位于隊列首部,通知隊列會將它發送到通知中心,然后依次的像注冊的所有觀察者派發通知。
每個線程有一個默認和 default notification center相關聯的的通知隊列。
如上圖所示主要是提供了一些方法給外部調用。
通過調用initWithNotificationCenter
和外部的NSNotificationCenter關聯起來,最終也是通過NSNotificationCenter來管理通知的發送、注冊。除此之外這里有兩個枚舉值需要特別注意一下。
- NSPostingStyle:用于配置通知什么時候發送
- NSPostASAP:在當前通知調用或者計時器結束發出通知
- NSPostWhenIdle:當runloop處于空閑時發出通知
- NSPostNow:在合并通知完成之后立即發出通知。
- NSNotificationCoalescing(注意這是一個NS_OPTIONS):用于配置如何合并通知
- NSNotificationNoCoalescing:不合并通知
- NSNotificationNoCoalescing:按照通知名字合并通知
- NSNotificationCoalescingOnSender:按照傳入的object合并通知
通知的實現原理
上面介紹完了關鍵對象的數據結構,可以用下圖歸納總結:
前面提到過NSNotification是一個類簇不能夠實例化的,當我們調用initWithName:object: userInfo:
方法的時候,系統內部會自己實現一個基于NSNotification的子類NSConcreteNotification。并且在這個子類中重寫了NSNotification定義的相關字段及方法。
NSNotificationCenter是中心管理類,實現較復雜。總的來講在NSNotificationCenter中定義了兩個Table,同時為了封裝觀察者信息,也定義了一個Observation保存觀察者信息。他們的結構體可以簡化如下:
typedef struct NCTbl {
Observation* wildcard; /* 保存既沒有沒有傳入通知名字也沒有傳入object的通知*/
MapTable nameless; /*保存沒有傳入通知名字的通知 */
MapTable named; /*保存傳入了通知名字的通知 */
} NCTable;
typedef struct Obs {
id observer; /* 保存接受消息的對象*/
SEL selector; /* 保存注冊通知時傳入的SEL*/
struct Obs* next; /* 保存注冊了同一個通知的下一個觀察者*/
struct NCTbl* link; /* 保存該Observation的Table*/
} Observation;
在NSNotificationCenter內部一共保存了兩張表。一張用于保存添加觀察者的時候傳入了NotifcationName的情況;一張用于保存添加觀察者的時候沒有傳入了NotifcationName的情況,下面分兩種情況分析。
Table
Named Table
先看一下表中保存的內容及Key,Value類型
在Named Table中,NotifcationName作為表的key,因為我們在注冊觀察者的時候是可以傳入一個參數object用于只監聽指定該對象發出的通知,并且一個通知可以添加多個觀察者,所以還需要一張表來保存object和Observer的對應關系。這張表的是key、Value分別是以object為Key,Observer為value。如何來實現保存多個觀察者的情況呢?用鏈表這種數據結構最合適不過了。
所以對于Named Table而已,最終的結構:
- 首先外層有一個Table,以通知名稱為Key。其Value同樣是一個Table(簡稱內Table).
- 為了實現可以傳入一個參數object用于只監聽指定該對象發出的通知,及一個通知可以添加多個觀察者。則內Table的以傳入的Object為Key,用鏈表來保存所有的觀察者,并且以這個鏈表為Value。
在實際開發中我們經常傳一個nil的object。這個時候系統會根據nil自動生產一個key(可以理解為一個nil_key)。相當于這個key對應的value(鏈表)保存的就是對于當前NotifcationName沒有傳入object的所有觀察者。當NotifcationName被發送時,所以在鏈表中的觀察者都會收到通知。
UnNamed Table
UnNamed Table結構比Named Table簡單很多。因為沒有NotifcationName作為Key。這里直接就以object為key。比Named Table少了一層Table嵌套。
如果在注冊觀察者的時候既沒有NotifcationName,同時沒有傳入Object。經過代碼實踐,所以的系統通知都會發送到注冊的對象這里。恰恰對應到上面提到的數據結構中的wildcard字段。
添加觀察者的流程
有了上面的基本的結構關系,再來看添加過程就會很簡單。總的過程就是按照上面的數據結構添加數據,中間會判斷Table及Observation結點是否存,不存在則創建新的,存在則直接使用。
首先在初始化NSNotificationCenter會創建一個對象,這個對象里面保存了NamedTable、UNnmedTable和一下其他信息。
所有的添加通知操作最后都會調用到addObserver: selector: name: object:
。
- 首先會根據傳入的參數,實例化一個Observation。這個Observation保存了觀察者對象、接收到通知觀察者對所執行的方法,由于Observation是一個鏈表,還保存了下一個Observation的地址。
- 根據是否傳入通知的Name選擇在Named Table還是UNamed Table操作。
- 如果傳入通知的Name,則會先去用Name去查找是否已經有對應的Value(注意這個時候返回的Value是一個Table)
- 如果沒有對應的Value,則創建一個新的Table,然后將這個Table以Name為Key添加到Named Table。如果有Value,那么直接去取出這個Table。
- 得到了保存Observation的Table之后,就通過傳入的object去拿對應的鏈表。如果object為空,會默認有一個key表示傳入object為空的情況,取的時候也會直接用這個key去取。表示所有任何地方發送通知都會監聽。
- 如果在保存Observation的Table中根據object作為key沒有找到對應的鏈表,則會創建一個節點,作為頭結點插入進去;如果找到了則直接在鏈表末尾插入之前實例化好的Observation。
在沒有傳入通知名字的情況和上面的過程類似,只不過是直接根據object去對應的鏈表而已。
如果既沒有傳入NotifcationName也沒有傳入Object。則這個觀察者會添加到wildcard(在介紹Table數據結構中提到夠)鏈表中。
發送通知的流程
發送通知的一般是調用postNotificationName:(NSNotificationName)aName object:(nullable id)anObject
來實現。
postNotificationName內部會實例化一個NSNotification來保存傳入的各種參數。根據之前介紹的數據結構,包含name、object和一個userinfo。
發送通知的流程總體來講就是根據NotifcationName查找到對應的Observer鏈表,然后遍歷整個鏈表,給每個Observer結點中保持存的對象及SEL,來像對象發送信息(也即是調用對象的SEL方法)
- 首先會定義一個數組ObserversArray來保存需要通知的Observer。之前在添加觀察者的時候把既沒有傳入NotifcationName也沒有傳入object保存在了wildcard。因為這樣的觀察者會監聽所有NotifcationName的通知,所以先把wildcard鏈表遍歷一遍,將其中的Observer加到數組中ObserversArray
- 找到以object為key的Observer鏈表。這個過程分為在Named Table中找,以及在UNamed Table中查找。然后將遍歷找到的鏈表,同樣加入到最開始創建的數組ObserversArray中。
- 至此所有關于NotifcationName的Observer(wildcard+UNamed Table+Named Table)已經加入到了數組ObserversArray。接下來就是遍歷這個ObserversArray數組,一次取出區中的Observer結點。因為這個幾點保存了觀察者對象以及selector。所以最終調用形式如下:
[observerNode->observer performSelector: o->selector withObject: notification];
這個方式也就能說明,發送通知的線程和接收通知的線程是同一個線程。在工作中經常為了保持在主線程中更新UI,所以經常會做接受通知的方法中用
dispatch_async(dispatch_get_main_queue(), ^{});
處理一下,以保障無論從什么線程發出的通知,都能在主線程中更新UI。
移除通知的流程
根據前面分析的添加觀察及發送通知的流程可以類比出移除通知的流程是如何的。掌握好核心就是操作兩個Table及一個鏈表。
結合上面講的相關數據結構,移除的通知的流程留給讀者自己去思考。
總結
其實分析NSNotification過程中間還有一些細節沒有考慮到。比如在整個Table非常非常大的時候如何保證查找的效率,而且這種場景在實際開發中也經常遇到,尤其是一些大型項目,隨隨便便就是成百上千個通知。關于這個問題,后面分析吧。