原創:知識點總結性文章
創作不易,請珍惜,之后會持續更新,不斷完善
個人比較喜歡做筆記和寫總結,畢竟好記性不如爛筆頭哈哈,這些文章記錄了我的IOS成長歷程,希望能與大家一起進步
溫馨提示:由于簡書不支持目錄跳轉,大家可通過command + F 輸入目錄標題后迅速尋找到你所需要的內容
目錄
- 一、使用
- 1、提供的屬性和方法
- 2、隊列的合并策略和發送時機
- 3、注意點
- 二、注冊通知源碼解析
- 1、存儲容器
- 2、解析注冊通知方法
- 3、判斷是否是同一個通知的3種情況
- 三、發送通知與刪除通知源碼解析
- 1、發送通知源碼解析
- 2、刪除通知源碼解析
- 四、異步通知
- 1、NSNotificationQueue的異步發送
- 2、把要發送的通知添加到隊列,等待發送
- 3、發送通知
- 4、主線程響應通知
- Demo
- 參考文獻
一、使用
1、提供的屬性和方法
NSNotification
- (NSString*) name; // 通知的name
- (id) object; // 攜帶的對象
- (NSDictionary*) userInfo; // 配置信息
NSNotificationCenter
// 添加通知
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
// 發送通知
- (void)postNotification:(NSNotification *)notification;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
// 刪除通知
- (void)removeObserver:(id)observer;
NSNotificationQueue
// 把通知添加到隊列中,NSPostingStyle是個枚舉
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
// 刪除通知,把滿足合并條件的通知從隊列中刪除
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;
用于異步發送消息的通知隊列,這個異步并不是開啟線程,而是把通知存到雙向鏈表實現的隊列里面,等待某個時機觸發。觸發時調用NSNotificationCenter
的發送接口進行發送通知,這么看NSNotificationQueue
最終還是調用NSNotificationCenter
進行消息的分發,另外NSNotificationQueue
是依賴runloop
的,所以如果線程的runloop
未開啟則無效。
2、隊列的合并策略和發送時機
把通知添加到隊列等待發送,同時提供了一些附加條件供開發者選擇,如:什么時候發送通知、如何合并通知等,系統給了如下定義。
表示通知的發送時機
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
NSPostWhenIdle = 1, // runloop空閑時發送通知
NSPostASAP = 2, // 盡快發送,這種情況稍微復雜,這種時機是穿插在每次事件完成期間來做的
NSPostNow = 3 // 立刻發送或者合并通知完成之后發送
};
通知合并的策略,有些時候同名通知只想存在一個,這時候就可以用到它了
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
NSNotificationNoCoalescing = 0, // 默認不合并
NSNotificationCoalescingOnName = 1, // 只要name相同,就認為是相同通知
NSNotificationCoalescingOnSender = 2 // object相同
};
3、注意點
頁面銷毀時不移除通知會崩潰嗎
可以,因為notificationcenter
對觀察者的引用是weak
,當觀察者釋放的時候,觀察者的指針值被置為nil
多次添加同一個通知會是什么結果?多次移除通知呢
會調用多次observer
的action
。多次移除沒有任何影響。
二、注冊通知源碼解析
1、存儲容器
NCTable是根容器,由NSNotificationCenter持有
NCTable
結構體中核心的三個變量:wildcard
、named
、nameless
,在源碼中直接用宏定義表示了:WILDCARD
、NAMELESS
、NAMED
。
typedef struct NCTbl
{
// 鏈表結構,保存既沒有name也沒有object的通知
Observation *wildcard;
// 存儲沒有name但是有object的通知
GSIMapTable nameless;
// 存儲帶有name的通知,不管有沒有object
GSIMapTable named;
...
} NCTable;
Observation是存儲觀察者和響應方法的結構體
typedef struct Obs
{
id observer;// 觀察者,接收通知的對象
SEL selector;// 響應方法
struct Obs *next;// 鏈表中的下一個Observation
...
} Observation;
2、解析注冊通知方法
- 判定是不是同一個通知要從
name
和object
區分,如果他們都相同則認為是同一個通知,后面包括查找邏輯、刪除邏輯都以此為基礎。 - 存儲過程并沒有做去重操作,這也解釋了為什么同一個通知注冊多次則響應多次
a、提供給外界使用的注冊通知方法
- observer:觀察者,即通知的接收者
- selector:接收到通知時的響應方法
-
name:通知
name
- object:攜帶對象
- (void) addObserver: (id)observer selector: (SEL)selector name: (NSString*)name object: (id)object
{
// 前置條件判斷
...
// 創建一個observation對象,持有觀察者和SEL,下面進行的所有邏輯就是為了存儲它
o = obsNew(TABLE, selector, observer);
...
}
b、情況一:如果name存在
if (name) {...}
? NAMED
是個宏,表示名為named
的字典。如果通知的name
存在,則以name
為key
從named
字典中取出值n
(這個n
其實被MapNode
包裝了一層,便于理解這里直接認為沒有包裝),這個n
還是個字典。
n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
? n
不存在,則先取緩存,如果緩存沒有則新建一個map
if (n == 0)
{
m = mapNew(TABLE);
GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
...
}
? n
存在則把值取出來賦值給m
else
{
m = (GSIMapTable)n->value.ptr;
}
? 然后以object
為key
,從字典中取出對應的值,這個值就是Observation
類型的鏈表,然后把剛開始創建的Observation
對象o
存儲進去。
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
if (n == 0)
{// 不存在,則創建
o->next = ENDOBS;
GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
}
else
{
list = (Observation*)n->value.ptr;
o->next = list->next;
list->next = o;
}
c、情況二:如果name為空,但object不為空
else if (object)
{
...
}
? 以object
為key
,從nameless
字典中取出對應的value
,value
是個鏈表結構。
n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
? 不存在則新建鏈表,并存到map
中
if (n == 0)
{
o->next = ENDOBS;
GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
}
? 存在則把值接到鏈表的節點上
else
{
...
}
d、情況三:name 和 object 都為空則存儲到wildcard鏈表中
else
{
o->next = WILDCARD;
WILDCARD = o;
}
3、判斷是否是同一個通知的3種情況
情況一:存在name(無論object是否存在)
如果注冊通知時傳入name
,那么會是一個雙層的存儲結構。首先找到NCTable
中的named
表,這個表存儲了name
的通知。接著以name
作為key
,找到value
,這個value
依然是一個map
。最后,map
的結構是以object
作為key
,Observation
對象為value
,這個Observation
對象的結構上面已經解釋,主要存儲了observer & SEL
。
情況二:只存在object
以object
為key
,從nameless
字典中取出value
,此value
是個Observation
類型的鏈表。接著把創建的Observation
類型的對象o
存儲到鏈表中。只存在object
時存儲只有一層,那就是object
和Observation
對象之間的映射。
情況三:沒有name和object
這種情況直接把Observation
對象存放在了Observation *wildcard
鏈表結構中。
三、發送通知與刪除通知源碼解析
1、發送通知源碼解析
發送通知的核心邏輯比較簡單,基本上就是查找和調用響應方法,從三個存儲容器中:named
、nameless
、wildcard
去查找對應的Observation
對象,然后通過performSelector
:逐一調用響應方法,這就完成了發送流程。
a、解析發送通知方法
- (void)postNotificationName: (NSString*)name object: (id)object userInfo: (NSDictionary*)info
{
...
}
? 構造一個GSNotification對象, GSNotification繼承了NSNotification
GSNotification *notification;
notification = (id)NSAllocateObject(concrete, 0, NSDefaultMallocZone());
notification->_name = [name copyWithZone: [self zone]];
notification->_object = [object retain];
notification->_info = [info retain];
? 進行發送操作
[self _postAndRelease: notification];
b、發送通知的核心函數
主要做了三件事:查找通知、發送、釋放資源。
- (void)_postAndRelease: (NSNotification*)notification
{
...
}
? 通過name & object
從named
、nameless
、wildcard
表中查找對應的通知(保存了observer
和sel
)。
...
? 執行發送,即調用performSelector
執行響應方法,從這里可以看出是同步的。
[o->observer performSelector: o->selector
withObject: notification];
? 釋放notification
對象。
RELEASE(notification);
2、刪除通知源碼解析
因為查找時做了這個鏈表的遍歷,所以刪除時會把重復的通知全都刪除掉
- (void)removeObserver: (id)observer
{
if (observer == nil) return;
[self removeObserver: observer name: nil object: nil];
}
查找時仍然以name
和object
為準,再加上observer
做區分。
- (void)removeObserver: (id)observer name: (NSString*)name object: (id)object
{
if (name == nil && object == nil && observer == nil)
return;
...
}
四、異步通知
1、NSNotificationQueue的異步發送
上面介紹的NSNotificationCenter
都是同步發送的,接受消息和發送消息是在一個線程里。這里介紹關于NSNotificationQueue
的異步發送,通過NSNotificationQueue
將通知添加到隊列當中,立即將控制權返回給調用者,在合適的時機發送通知,從而不會阻塞當前的調用。從線程的角度看并不是真正的異步發送,或可稱為延時發送,它是利用了runloop
的時機來觸發的,所以如果在其他子線程使用NSNotificationQueue
,需要開啟runloop
。由于最終還是通過NSNotificationCenter
進行發送通知,所以從這個角度講它還是同步的。所謂異步,指的是非實時發送而是在合適的時機發送,并沒有開啟異步線程。
2、把要發送的通知添加到隊列,等待發送
NSPostingStyle
和 coalesceMask
在上面的類結構中有介紹。modes
這個就和runloop
有關了,指的是runloop
的mode
。
- (void) enqueueNotification: (NSNotification*)notification
postingStyle: (NSPostingStyle)postingStyle
coalesceMask: (NSUInteger)coalesceMask
forModes: (NSArray*)modes
{
...
}
? 根據coalesceMask
參數判斷是否合并通知
if (coalesceMask != NSNotificationNoCoalescing)
{
[self dequeueNotificationsMatching: notification
coalesceMask: coalesceMask];
}
? 接著根據postingStyle
參數,判斷通知發送的時機
switch (postingStyle)
{
...
}
? runloop
立即回調通知方法,同步發送
case NSPostNow:
{
// 如果是立馬發送,則調用NSNotificationCenter進行發送
[_center postNotification: notification];
}
? runloop
在執行timer
事件或sources
事件的時候回調通知方法,異步發送
case NSPostASAP:
// 添加到_asapQueue隊列,等待發送
add_to_queue(_asapQueue, notification, modes, _zone);
? runloop
空閑的時候回調通知方法,異步發送
case NSPostWhenIdle:
// 添加到_idleQueue隊列,等待發送
add_to_queue(_idleQueue, notification, modes, _zone);
3、發送通知
runloop
觸發某個時機,調用GSPrivateNotifyASAP()
和GSPrivateNotifyIdle()
方法,這兩個方法最終都調用了notify()
方法。notify()
所做的事情就是調用NSNotificationCenter
的postNotification:
進行發送通知。
a、發送_asapQueue中的通知
void GSPrivateNotifyASAP(NSString *mode)
{
notify(item->queue->_center,
item->queue->_asapQueue,
mode,
item->queue->_zone);
}
b、發送_idleQueue中的通知
void GSPrivateNotifyIdle(NSString *mode)
{
notify(item->queue->_center,
item->queue->_idleQueue,
mode,
item->queue->_zone);
}
c、循環遍歷發送通知
static void notify(NSNotificationCenter *center,
NSNotificationQueueList *list,
NSString *mode, NSZone *zone)
{
for (pos = 0; pos < len; pos++)
{
NSNotification *n = (NSNotification*)ptr[pos];
[center postNotification: n];
RELEASE(n);
}
}
4、主線程響應通知
異步線程發送通知則響應函數也是在異步線程,如果執行UI刷新相關的話就會出現問題,那么如何保證在主線程響應通知呢?可以使用addObserverForName: object: queue: usingBlock
方法注冊通知,指定在mainqueue
上響應block
。
Demo
Demo在我的Github上,歡迎下載。
BasicsDemo