iOS:通知

關(guān)鍵類(lèi)

NSNotification

用于描述通知的類(lèi)畦粮,一個(gè)NSNotification對(duì)象就包含了一條通知的信息露泊。

// 通知名稱(chēng)
@property (readonly, copy) NSNotificationName name;
// 通知攜帶的對(duì)象
@property (nullable, readonly, retain) id object;
// 發(fā)送通知時(shí) 配置的信息
@property (nullable, readonly, copy) NSDictionary *userInfo;

// 初始化方法
- (instancetype)initWithName:(NSNotificationName)name object:(nullable id)object userInfo:(nullable NSDictionary *)userInfo喉镰;
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject;
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;

NSNotificationCenter

是個(gè)單例類(lèi),負(fù)責(zé)管理通知的創(chuàng)建和發(fā)送滤淳,屬于最核心的類(lèi)了梧喷。它主要做三件事:
1、添加通知
2脖咐、發(fā)送通知
3铺敌、移除通知

// 全局唯一NSNotificationCenter對(duì)象
@property (class, readonly, strong) NSNotificationCenter *defaultCenter;

// 在通知中心添加一個(gè)通知
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;

// 發(fā)出一個(gè)通知
- (void)postNotification:(NSNotification *)notification;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;

// 移除一個(gè)通知
- (void)removeObserver:(id)observer;
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;

// 添加一個(gè)通知觀察者,并在收到指定通知時(shí)執(zhí)行指定的代碼塊
// 指定代碼塊的操作隊(duì)列屁擅。如果為 nil偿凭,則在發(fā)送通知的線程上執(zhí)行代碼塊。
// 返回的觀察者對(duì)象 用于移除時(shí)使用
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (NS_SWIFT_SENDABLE ^)(NSNotification *note))block ;

注意:
1派歌、添加通知時(shí)observer和sel都不可為空弯囊,通知名稱(chēng)name和攜帶對(duì)象object可以為空痰哨。
2、注冊(cè)通知時(shí)應(yīng)避免重復(fù)注冊(cè)匾嘱,會(huì)導(dǎo)致重復(fù)處理通知信息
3斤斧、使用block時(shí),block內(nèi)可能會(huì)持有外部對(duì)象霎烙,避免循環(huán)引用撬讽。
4、消息發(fā)送類(lèi)型需要和注冊(cè)時(shí)一致悬垃。
若注冊(cè)時(shí)同時(shí)指定了名稱(chēng)和攜帶對(duì)象游昼,發(fā)到那個(gè)消息時(shí)也要指定,否則無(wú)法收到消息(更多原因會(huì)在底層分析時(shí)介紹)
5尝蠕、移除通知烘豌,ios9及macos10.11之后不需要手動(dòng)調(diào)用,dealloc已經(jīng)自動(dòng)處理看彼。

NSNotificationQueue

通知隊(duì)列廊佩,用于異步發(fā)送消息。
這個(gè)異步并不是開(kāi)啟線程闲昭,而是把通知存到雙向鏈表實(shí)現(xiàn)的隊(duì)列里面罐寨,
等待某個(gè)時(shí)機(jī)觸發(fā)時(shí)調(diào)用NSNotificationCenter的發(fā)送接口進(jìn)行發(fā)送通知。
NSNotificationQueue最終還是調(diào)用NSNotificationCenter進(jìn)行消息的分發(fā)序矩。

NSNotificationQueue主要做了兩件事:
1、添加通知到隊(duì)列
2跋破、刪除通知

// 獲取當(dāng)前當(dāng)前線程綁定的通知消息隊(duì)列
@property (class, readonly, strong) NSNotificationQueue *defaultQueue;
// 指定通知中心創(chuàng)建通知隊(duì)列
- (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter ;

// 把通知添加到隊(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ā)送時(shí)機(jī)

typedef NS_ENUM(NSUInteger, NSPostingStyle) {
    NSPostWhenIdle = 1, // runloop空閑時(shí)發(fā)送通知
    NSPostASAP = 2,     // 當(dāng)前通知調(diào)用或者計(jì)時(shí)器結(jié)束發(fā)出通知
    NSPostNow = 3       // 立刻發(fā)送或在合并通知完成之后發(fā)送
};

通知的合并策略

// 有些時(shí)候同名通知只想存在一個(gè),這時(shí)候就可以用到它了
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
    NSNotificationNoCoalescing = 0,// 默認(rèn)不合并
    NSNotificationCoalescingOnName = 1,// 按照通知name合并毒返,相同就合并
    NSNotificationCoalescingOnSender = 2// 按照傳入的object合并租幕,相同就合并
};

底層分析

由于蘋(píng)果沒(méi)有對(duì)相關(guān)源碼開(kāi)放,所以以GNUStep源碼為基礎(chǔ)進(jìn)行研究拧簸,雖然GNUStep不是蘋(píng)果官方的源碼劲绪,但很具有參考意義,根據(jù)實(shí)現(xiàn)原理來(lái)猜測(cè)和實(shí)踐盆赤,更重要的還可以學(xué)習(xí)觀察者模式的架構(gòu)設(shè)計(jì)贾富。

1、_GSIMapTable

 *  A rough picture is include below:
 *   
 *  
 *   This is the map                C - array of the buckets
 *   +---------------+             +---------------+
 *   | _GSIMapTable  |      /----->| nodeCount     |  
 *   |---------------|     /       | firstNode ----+--\  
 *   | buckets    ---+----/        | ..........    |  |
 *   | bucketCount  =| size of --> | nodeCount     |  |
 *   | nodeChunks ---+--\          | firstNode     |  |
 *   | chunkCount  =-+\ |          |     .         |  | 
 *   |   ....        || |          |     .         |  |
 *   +---------------+| |          | nodeCount     |  |
 *                    | |          | fistNode      |  | 
 *                    / |          +---------------+  | 
 *         ----------   v                             v
 *       /       +----------+      +---------------------------+ 
 *       |       |  * ------+----->| Node1 | Node2 | Node3 ... | a chunk
 *   chunkCount  |  * ------+--\   +---------------------------+
 *  is size of = |  .       |   \  +-------------------------------+
 *               |  .       |    ->| Node n | Node n + 1 | ...     | another
 *               +----------+      +-------------------------------+
 *               array pointing
 *               to the chunks
 *
#if !defined(GSI_MAP_TABLE_T)
typedef struct _GSIMapBucket GSIMapBucket_t;
typedef struct _GSIMapNode GSIMapNode_t;

typedef GSIMapBucket_t *GSIMapBucket;
typedef GSIMapNode_t *GSIMapNode;
#endif

struct  _GSIMapNode {
  GSIMapNode    nextInBucket;   /* Linked list of bucket.   */
  GSIMapKey key;
#if GSI_MAP_HAS_VALUE
  GSIMapVal value;
#endif
};

struct  _GSIMapBucket {
  uintptr_t nodeCount;  /* Number of nodes in bucket.   */
  GSIMapNode    firstNode;  /* The linked list of nodes.    */
};

#if defined(GSI_MAP_TABLE_T)
typedef GSI_MAP_TABLE_T *GSIMapTable;
#else
typedef struct _GSIMapTable GSIMapTable_t;
typedef GSIMapTable_t *GSIMapTable;

struct  _GSIMapTable {
  NSZone    *zone;
  uintptr_t nodeCount;  /* Number of used nodes in map. */
  uintptr_t bucketCount;    /* Number of buckets in map.    */
  GSIMapBucket  buckets;    /* Array of buckets.        */
  GSIMapNode    freeNodes;  /* List of unused nodes.    */
  uintptr_t chunkCount; /* Number of chunks in array.   */
  GSIMapNode    *nodeChunks;    /* Chunks of allocated memory.  */
  uintptr_t increment;
#ifdef  GSI_MAP_EXTRA
  GSI_MAP_EXTRA extra;
#endif
};

_GSIMapTable結(jié)構(gòu)體內(nèi)的變量理解:
1牺六、buckets:存儲(chǔ)著GSIMapBucket對(duì)象的數(shù)組颤枪,可以堪稱(chēng)是一個(gè)單向鏈表,每個(gè)GSIMapBucket中的firstNode->nextInBucket表示下一個(gè)bucket淑际,依次連接畏纲。
2扇住、bucketCount表示buckets的數(shù)量。
3盗胀、nodeChunks艘蹋,表示一個(gè)數(shù)組指針,數(shù)組存儲(chǔ)所有單鏈表的首個(gè)元素node票灰。
4女阀、chunkCount 表示數(shù)組大小吹零。
5绩社、freeNodes是需要釋放的元素,是一個(gè)單向鏈表塔嬉。
_GSIMapTable其實(shí)就是一個(gè)hash表結(jié)構(gòu)屈糊,既可以以數(shù)組的形式取到每個(gè)單向鏈表首元素的榛,也可以以鏈表形式取得。通過(guò)數(shù)組能夠方便取到每個(gè)單向鏈表逻锐,再利用鏈表結(jié)構(gòu)方便增刪夫晌。

存儲(chǔ)容器NCTbl

// 根容器,NSNotificationCenter持有
typedef struct NCTbl {
  Observation       *wildcard;  /* 鏈表結(jié)構(gòu)昧诱,保存既沒(méi)有name也沒(méi)有object的通知 */
  GSIMapTable       nameless;   /* 存儲(chǔ)沒(méi)有name但是有object的通知 */
  GSIMapTable       named;      /* 存儲(chǔ)帶有name的通知晓淀,不管有沒(méi)有object  */
    ...
} NCTable;

// Observation 存儲(chǔ)觀察者和響應(yīng)結(jié)構(gòu)體,基本的存儲(chǔ)單元
typedef struct  Obs {
  id        observer;   /* 觀察者盏档,接收通知的對(duì)象  */
  SEL       selector;   /* 響應(yīng)方法     */
  struct Obs    *next;      /* Next item in linked list.    */
  ...
} Observation;

wildcard:Observation結(jié)構(gòu)凶掰,存儲(chǔ)既沒(méi)有name也沒(méi)有object的通知
nameless:GSIMapTable結(jié)構(gòu),存儲(chǔ)存儲(chǔ)沒(méi)有name但是有object的通知
named:GSIMapTable蜈亩,存儲(chǔ)帶有name的通知懦窘,不管有沒(méi)有object的通知。

注冊(cè)通知

selector: (SEL)selector注冊(cè)通知

/*
observer:觀察者稚配,即通知的接收者
selector:接收到通知時(shí)的響應(yīng)方法
name: 通知name
object:攜帶對(duì)象
*/
- (void) addObserver: (id)observer
            selector: (SEL)selector
                name: (NSString*)name 
                object: (id)object {
  // 前置條件判斷
  ......

  // 創(chuàng)建一個(gè)observation對(duì)象畅涂,持有觀察者和SEL,下面進(jìn)行的所有邏輯就是為了存儲(chǔ)它
  o = obsNew(TABLE, selector, observer);

/*======= case1: 如果name存在 =======*/
  if (name) {
    //-------- NAMED是個(gè)宏道川,表示名為named字典午衰。以name為key,從named表中獲取對(duì)應(yīng)的mapTable
      n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
      if (n == 0) { // 不存在冒萄,則創(chuàng)建 
          m = mapNew(TABLE); // 先取緩存臊岸,如果緩存沒(méi)有則新建一個(gè)map
          GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
          ...
      }
      else { // 存在則把值取出來(lái) 賦值給m
          m = (GSIMapTable)n->value.ptr;
      }
    //-------- 以object為key,從字典m中取出對(duì)應(yīng)的value宦言,其實(shí)value被MapNode的結(jié)構(gòu)包裝了一層扇单,這里不追究細(xì)節(jié)
      n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
      if (n == 0) {// 不存在,則創(chuàng)建 
          o->next = ENDOBS;
          GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
      }
      else {
          list = (Observation*)n->value.ptr;
          o->next = list->next;
          list->next = o;
      }
    }
/*======= case2:如果name為空奠旺,但object不為空 =======*/
  else if (object) {
    // 以object為key蜘澜,從nameless字典中取出對(duì)應(yīng)的value施流,value是個(gè)鏈表結(jié)構(gòu)
      n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
      // 不存在則新建鏈表,并存到map中
      if (n == 0) { 
          o->next = ENDOBS;
          GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
      }
      else { // 存在 則把值接到鏈表的節(jié)點(diǎn)上
        ...
      }
    }
/*======= case3:name 和 object 都為空 則存儲(chǔ)到wildcard鏈表中 =======*/
  else {
      o->next = WILDCARD;
      WILDCARD = o;
  }
}

從上面介紹的存儲(chǔ)容器中我們了解到NCTable結(jié)構(gòu)體中核心的三個(gè)變量以及功能:wildcard鄙信、named瞪醋、nameless,在源碼中直接用宏定義表示了:WILDCARD装诡、NAMELESS银受、NAMED。

case1: 存在name(無(wú)論object是否存在)

1鸦采、注冊(cè)通知宾巍,如果通知的name存在,則以name為key從named字典中取出值n(這個(gè)n其實(shí)被MapNode包裝了一層渔伯,便于理解這里直接認(rèn)為沒(méi)有包裝)顶霞,這個(gè)n還是個(gè)字典,各種判空新建邏輯不討論锣吼。
2选浑、然后以object為key,從字典n中取出對(duì)應(yīng)的值玄叠,這個(gè)值就是Observation類(lèi)型(后面簡(jiǎn)稱(chēng)obs)的鏈表古徒,然后把剛開(kāi)始創(chuàng)建的obs對(duì)象o存儲(chǔ)進(jìn)去。

數(shù)據(jù)結(jié)構(gòu)關(guān)系圖

如果注冊(cè)通知時(shí)傳入name读恃,那么會(huì)是一個(gè)雙層的存儲(chǔ)結(jié)構(gòu)
1隧膘、找到NCTable中的named表,這個(gè)表存儲(chǔ)了還有name的通知
2寺惫、以name作為key舀寓,找到value,這個(gè)value依然是一個(gè)map
3肌蜻、map的結(jié)構(gòu)是以object作為key,obs對(duì)象為value必尼,這個(gè)obs對(duì)象的結(jié)構(gòu)上面已經(jīng)解釋?zhuān)饕鎯?chǔ)了observer & SEL

case2: 只存在object

1蒋搜、以object為key,從nameless字典中取出value判莉,此value是個(gè)obs類(lèi)型的鏈表豆挽。
2、把創(chuàng)建的obs類(lèi)型的對(duì)象o存儲(chǔ)到鏈表中券盅。

數(shù)據(jù)結(jié)構(gòu)關(guān)系圖

只存在object時(shí)存儲(chǔ)只有一層帮哈,那就是object和obs對(duì)象之間的映射

case3: 沒(méi)有name和object

這種情況直接把obs對(duì)象存放在了Observation *wildcard 鏈表結(jié)構(gòu)中。

block - 注冊(cè)通知

- (id) addObserverForName: (NSString *)name 
                   object: (id)object 
                    queue: (NSOperationQueue *)queue 
               usingBlock: (GSNotificationBlock)block
{
    // 創(chuàng)建一個(gè)臨時(shí)觀察者
    GSNotificationObserver *observer = 
        [[GSNotificationObserver alloc] initWithQueue: queue block: block];
    // 調(diào)用了@selector的注冊(cè)方法
    [self addObserver: observer 
             selector: @selector(didReceiveNotification:) 
                 name: name 
               object: object];

    return observer;
}

- (void) didReceiveNotification: (NSNotification *)notif {
    if (_queue != nil) {
        GSNotificationBlockOperation *op = [[GSNotificationBlockOperation alloc] 
            initWithNotification: notif block: _block];
        [_queue addOperation: op];
    } else {
        CALL_BLOCK(_block, notif);
    }
}

這個(gè)接口依賴(lài)于selector注冊(cè)锰镀,娘侍,只是多了一層代理觀察者GSNotificationObserver咖刃,
1、創(chuàng)建一個(gè)GSNotificationObserver類(lèi)型的對(duì)象observer憾筏,并把queue和block保存下來(lái)
2嚎杨、調(diào)用接口1進(jìn)行通知的注冊(cè)
3、接收到通知時(shí)會(huì)響應(yīng)observer的didReceiveNotification:方法氧腰,然后在didReceiveNotification:中把block拋給指定的queue去執(zhí)行枫浙。

從上述介紹可以總結(jié):
1、存儲(chǔ)是以name和object為維度的古拴,即判定是不是同一個(gè)通知要從name和object區(qū)分箩帚,如果他們都相同則認(rèn)為是同一個(gè)通知,后面包括查找邏輯黄痪、刪除邏輯都是以這兩個(gè)為維度的紧帕,問(wèn)題列表中的第九題也迎刃而解了。
2满力、理解數(shù)據(jù)結(jié)構(gòu)的設(shè)計(jì)是整個(gè)通知機(jī)制的核心焕参,其他功能只是在此基礎(chǔ)上擴(kuò)展了一些邏輯。
3油额、存儲(chǔ)過(guò)程并沒(méi)有做去重操作叠纷,這也解釋了為什么同一個(gè)通知注冊(cè)多次則響應(yīng)多次。

發(fā)送通知

// 發(fā)送通知
- (void) postNotificationName: (NSString*)name
               object: (id)object
             userInfo: (NSDictionary*)info
{
// 構(gòu)造一個(gè)GSNotification對(duì)象潦嘶, GSNotification繼承了NSNotification
  GSNotification    *notification;
  notification = (id)NSAllocateObject(concrete, 0, NSDefaultMallocZone());
  notification->_name = [name copyWithZone: [self zone]];
  notification->_object = [object retain];
  notification->_info = [info retain];

  // 進(jìn)行發(fā)送操作
  [self _postAndRelease: notification];
}
//發(fā)送通知的核心函數(shù)涩嚣,主要做了三件事:查找通知、發(fā)送掂僵、釋放資源
- (void) _postAndRelease: (NSNotification*)notification {
    //step1: 從named航厚、nameless、wildcard表中查找對(duì)應(yīng)的通知
    ...
    //step2:執(zhí)行發(fā)送锰蓬,即調(diào)用performSelector執(zhí)行響應(yīng)方法幔睬,從這里可以看出是同步的
    [o->observer performSelector: o->selector
                    withObject: notification];
    //step3: 釋放資源
    RELEASE(notification);
}

上述代碼注釋說(shuō)的很清晰了,主要做了三件事
1芹扭、通過(guò)name & object 查找到所有的obs對(duì)象(保存了observer和sel)麻顶,放到數(shù)組中。
2舱卡、通過(guò)performSelector:逐一調(diào)用sel辅肾,這是個(gè)同步操作。
3轮锥、釋放notification對(duì)象矫钓。

發(fā)送過(guò)程的概述:
1、從三個(gè)存儲(chǔ)容器中:named、nameless新娜、wildcard去查找對(duì)應(yīng)的obs對(duì)象赵辕。
2、然后通過(guò)performSelector:逐一調(diào)用響應(yīng)方法杯活,這就完成了發(fā)送流程匆帚。

核心點(diǎn):
1、同步發(fā)送
2旁钧、遍歷所有列表吸重,即注冊(cè)多次通知就會(huì)響應(yīng)多次

刪除通知

1、查找時(shí)仍然以name和object為維度的歪今,再加上observer做區(qū)分嚎幸。
2、因?yàn)椴檎視r(shí)做了這個(gè)鏈表的遍歷寄猩,所以刪除時(shí)會(huì)把重復(fù)的通知全都刪除掉嫉晶。

更多源碼下載GNUStep

異步通知

入隊(duì)

入隊(duì)
下面為精簡(jiǎn)版的源碼,看源碼的注釋?zhuān)旧夏苊靼状笾逻壿?br> 1田篇、根據(jù)coalesceMask參數(shù)判斷是否合并通知替废。
2、接著根據(jù)postingStyle參數(shù)泊柬,判斷通知發(fā)送的時(shí)機(jī)椎镣,
如果不是立即發(fā)送則把通知加入到隊(duì)列中:_asapQueue、_idleQueue兽赁。

核心點(diǎn):
1状答、隊(duì)列是雙向鏈表實(shí)現(xiàn)
2、當(dāng)postingStyle值是立即發(fā)送時(shí)刀崖,調(diào)用的是NSNotificationCenter進(jìn)行發(fā)送的惊科,所以NSNotificationQueue還是依賴(lài)NSNotificationCenter進(jìn)行發(fā)送

/*
* 把要發(fā)送的通知添加到隊(duì)列,等待發(fā)送
* NSPostingStyle 和 coalesceMask在上面的類(lèi)結(jié)構(gòu)中有介紹
* modes這個(gè)就和runloop有關(guān)了亮钦,指的是runloop的mode
*/ 
- (void) enqueueNotification: (NSNotification*)notification
        postingStyle: (NSPostingStyle)postingStyle
        coalesceMask: (NSUInteger)coalesceMask
            forModes: (NSArray*)modes
{
    ......
  // 判斷是否需要合并通知
  if (coalesceMask != NSNotificationNoCoalescing) {
      [self dequeueNotificationsMatching: notification
                coalesceMask: coalesceMask];
  }
  switch (postingStyle) {
      case NSPostNow: {
        ...
        // 如果是立馬發(fā)送馆截,則調(diào)用NSNotificationCenter進(jìn)行發(fā)送
         [_center postNotification: notification];
         break;
      }
      case NSPostASAP:
        // 添加到_asapQueue隊(duì)列,等待發(fā)送
        add_to_queue(_asapQueue, notification, modes, _zone);
        break;

      case NSPostWhenIdle:
        // 添加到_idleQueue隊(duì)列蜂莉,等待發(fā)送
        add_to_queue(_idleQueue, notification, modes, _zone);
        break;
    }
}

發(fā)送通知

這里截取了發(fā)送通知的核心代碼孙咪,這個(gè)發(fā)送通知邏輯如下:
1、runloop觸發(fā)某個(gè)時(shí)機(jī)巡语,調(diào)用GSPrivateNotifyASAP()和GSPrivateNotifyIdle()方法,這兩個(gè)方法最終都調(diào)用了notify()方法淮菠。
2男公、notify()所做的事情就是調(diào)用NSNotificationCenter的postNotification:進(jìn)行發(fā)送通知。

static void notify(NSNotificationCenter *center, 
                   NSNotificationQueueList *list,
                   NSString *mode, NSZone *zone)
{
    ......
    // 循環(huán)遍歷發(fā)送通知
    for (pos = 0; pos < len; pos++)
    {
      NSNotification    *n = (NSNotification*)ptr[pos];

      [center postNotification: n];
      RELEASE(n);
    }
    ......  
}
// 發(fā)送_asapQueue中的通知
void GSPrivateNotifyASAP(NSString *mode)
{
    notify(item->queue->_center,
        item->queue->_asapQueue,
        mode,
        item->queue->_zone);
}
// 發(fā)送_idleQueue中的通知
void GSPrivateNotifyIdle(NSString *mode)
{
    notify(item->queue->_center,
        item->queue->_idleQueue,
        mode,
        item->queue->_zone);
}

NSNotificationQueue總結(jié):
1、依賴(lài)runloop枢赔,所以如果在其他子線程使用NSNotificationQueue澄阳,需要開(kāi)啟runloop。
2踏拜、最終還是通過(guò)NSNotificationCenter進(jìn)行發(fā)送通知碎赢,所以這個(gè)角度講它還是同步的。
3速梗、所謂異步肮塞,指的是非實(shí)時(shí)發(fā)送而是在合適的時(shí)機(jī)發(fā)送,并沒(méi)有開(kāi)啟異步線程姻锁。

主線程響應(yīng)通知

異步線程發(fā)送通知?jiǎng)t響應(yīng)函數(shù)也是在異步線程枕赵,如果執(zhí)行UI刷新相關(guān)的話就會(huì)出問(wèn)題,那么如何保證在主線程響應(yīng)通知呢位隶?
其實(shí)也是比較常見(jiàn)的問(wèn)題了拷窜,基本上解決方式如下幾種:
1、使用addObserverForName: object: queue: usingBlock方法注冊(cè)通知涧黄,指定在mainqueue上響應(yīng)block篮昧。
2、在主線程注冊(cè)一個(gè)machPort笋妥,它是用來(lái)做線程通信的懊昨,當(dāng)在異步線程收到通知,然后給machPort發(fā)送消息挽鞠,這樣肯定是在主線程處理的疚颊。

問(wèn)題列表

1、通知的實(shí)現(xiàn)原理(結(jié)構(gòu)設(shè)計(jì)信认、通知如何存儲(chǔ)的材义、name&observer&SEL之間的關(guān)系等)

通知里面主要有三個(gè)類(lèi)NSNotification通知對(duì)象,包括name嫁赏,object其掂,userinfo
NSNotificationCenter,通知中心潦蝇,負(fù)責(zé)添加款熬、發(fā)送、移除通知攘乒。
NSNotificationQueue贤牛,通知隊(duì)列,負(fù)責(zé)某些時(shí)機(jī)觸發(fā)調(diào)用通知则酝,最后調(diào)用還是NSNotificationCenter通知中心 post通知殉簸。
通知隊(duì)列里的異步,實(shí)際上不是真正的異步,更多是延時(shí)發(fā)送般卑,利用了runloop的時(shí)機(jī)來(lái)觸發(fā)武鲁。

根據(jù)開(kāi)源代碼GNUStep推測(cè),通知時(shí)結(jié)構(gòu)體蝠检,通過(guò)雙向鏈表進(jìn)行數(shù)據(jù)存儲(chǔ)

// 根容器沐鼠,NSNotificationCenter持有
typedef struct NCTbl {
  Observation       *wildcard;  /* 鏈表結(jié)構(gòu),保存既沒(méi)有name也沒(méi)有object的通知 */
  GSIMapTable       nameless;   /* 存儲(chǔ)沒(méi)有name但是有object的通知 */
  GSIMapTable       named;      /* 存儲(chǔ)帶有name的通知叹谁,不管有沒(méi)有object  */
    ...
} NCTable;

// Observation 存儲(chǔ)觀察者和響應(yīng)結(jié)構(gòu)體饲梭,基本的存儲(chǔ)單元
typedef struct  Obs {
  id        observer;   /* 觀察者,接收通知的對(duì)象  */
  SEL       selector;   /* 響應(yīng)方法     */
  struct Obs    *next;      /* Next item in linked list.    */
  ...
} Observation;

通知是以key value的形式存儲(chǔ),
需要重點(diǎn)強(qiáng)調(diào):
通知以 name和object兩個(gè)緯度來(lái)存儲(chǔ)相關(guān)通知內(nèi)容,也就是我們添加通知的時(shí)候傳入的兩個(gè)不同的方法.
name&observer&SEL之間的關(guān)系:name作為key, observer作為觀察者對(duì)象,當(dāng)合適時(shí)機(jī)觸發(fā)就會(huì)調(diào)用observer的SEL

2本慕、通知的發(fā)送時(shí)同步的排拷,還是異步的

同步發(fā)送,因?yàn)橐{(diào)用消息轉(zhuǎn)發(fā).
所謂異步,指的是非實(shí)時(shí)發(fā)送而是在合適的時(shí)機(jī)發(fā)送锅尘,并沒(méi)有開(kāi)啟異步線程.

3监氢、NSNotificationCenter 接受消息和發(fā)送消息是在一個(gè)線程里嗎?如何異步發(fā)送消息

是的, 異步線程發(fā)送通知?jiǎng)t響應(yīng)函數(shù)也是在異步線程.
異步發(fā)送通知可以開(kāi)啟異步線程發(fā)送即可.

4藤违、NSNotificationQueue是異步還是同步發(fā)送浪腐?在哪個(gè)線程響應(yīng)?

// 表示通知的發(fā)送時(shí)機(jī)
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
    NSPostWhenIdle = 1, // runloop空閑時(shí)發(fā)送通知
    NSPostASAP = 2, // 盡快發(fā)送,這種時(shí)機(jī)是穿插在每次事件完成期間來(lái)做的
    NSPostNow = 3 // 立刻發(fā)送或者合并通知完成之后發(fā)送
};

NSPostWhenIdle:異步發(fā)送
NSPostASAP:異步發(fā)送
NSPostNow:同步發(fā)送

NSNotificationCenter都是同步發(fā)送的顿乒,
而關(guān)于NSNotificationQueue的異步發(fā)送议街,從線程的角度看并不是真正的異步發(fā)送,或可稱(chēng)為延時(shí)發(fā)送璧榄,它是利用了runloop的時(shí)機(jī)來(lái)觸發(fā)的特漩。
異步線程發(fā)送通知?jiǎng)t響應(yīng)函數(shù)也是在異步線程,主線程發(fā)送則在主線程。

5骨杂、NSNotificationQueue和runloop的關(guān)系

NSNotificationQueue依賴(lài)runloop. 因?yàn)橥ㄖ?duì)列要在runloop回調(diào)的某個(gè)時(shí)機(jī)調(diào)用通知中心發(fā)送通知涂身。

6、如何保證通知接收的線程在主線程

兩種方式:
1搓蚪、系統(tǒng)接受通知的API指定隊(duì)列
2蛤售、NSMachPort的方式 通過(guò)在主線程的 runloop 中添加 machPort,設(shè)置這個(gè) port 的 delegate妒潭,通過(guò)這個(gè) Port 其他線程可以跟主線程通信悴能,在這個(gè) port 的代理回調(diào)中執(zhí)行的代碼肯定在主線程中運(yùn)行,所以雳灾,在這里調(diào)用 NSNotificationCenter 發(fā)送通知即可

7漠酿、頁(yè)面銷(xiāo)毀時(shí)不移除通知會(huì)崩潰嗎?

iOS9.0之前,會(huì)crash谎亩,原因:通知中心對(duì)觀察者的引用是 unsafe_unretained记靡,導(dǎo)致當(dāng)觀察者釋放的時(shí)候谈竿,觀察者的指針值并不為nil,出現(xiàn)野指針.
iOS9.0之后摸吠,不會(huì)crash,原因:通知中心對(duì)觀察者的引用是 weak嚎花。

8寸痢、多次添加同一個(gè)通知會(huì)是什么結(jié)果?多次移除通知呢

多次添加同一個(gè)通知紊选,會(huì)導(dǎo)致發(fā)送一次這個(gè)通知的時(shí)候啼止,響應(yīng)多次通知回調(diào)。
多次移除通知不會(huì)產(chǎn)生crash兵罢。

9献烦、下面的方式能接收到通知嗎?為什么

// 發(fā)送通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];
// 接收通知
[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];

不能卖词。根據(jù)推測(cè)的通知中心存儲(chǔ)通知觀察者的結(jié)構(gòu)巩那。

// 根容器,NSNotificationCenter持有
typedef struct NCTbl {
  Observation  *wildcard;    /* 鏈表結(jié)構(gòu)此蜈,保存既沒(méi)有name也沒(méi)有object的通知 */
  GSIMapTable nameless;    /* 存儲(chǔ)沒(méi)有name但是有object的通知    */
  GSIMapTable named;        /* 存儲(chǔ)帶有name的通知即横,不管有沒(méi)有object    */
    ...
} NCTable;

當(dāng)添加通知監(jiān)聽(tīng)的時(shí)候,我們傳入了name和object裆赵,所以东囚,觀察者的存儲(chǔ)鏈表是這樣的:named表:key(name) : value->key(object) : value(Observation)
因此在發(fā)送通知的時(shí)候,如果只傳入name而并沒(méi)有傳入object战授,是找不到Observation的页藻,也就不能執(zhí)行觀察者回調(diào)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末植兰,一起剝皮案震驚了整個(gè)濱河市份帐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌钉跷,老刑警劉巖弥鹦,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異爷辙,居然都是意外死亡彬坏,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)膝晾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)栓始,“玉大人,你說(shuō)我怎么就攤上這事血当』米” “怎么了禀忆?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)落恼。 經(jīng)常有香客問(wèn)我箩退,道長(zhǎng),這世上最難降的妖魔是什么佳谦? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任戴涝,我火速辦了婚禮,結(jié)果婚禮上钻蔑,老公的妹妹穿的比我還像新娘啥刻。我一直安慰自己,他們只是感情好咪笑,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布可帽。 她就那樣靜靜地躺著,像睡著了一般窗怒。 火紅的嫁衣襯著肌膚如雪映跟。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,950評(píng)論 1 291
  • 那天兜粘,我揣著相機(jī)與錄音申窘,去河邊找鬼。 笑死孔轴,一個(gè)胖子當(dāng)著我的面吹牛剃法,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播路鹰,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼贷洲,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了晋柱?” 一聲冷哼從身側(cè)響起优构,我...
    開(kāi)封第一講書(shū)人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎雁竞,沒(méi)想到半個(gè)月后钦椭,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡碑诉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年彪腔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片进栽。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡德挣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出快毛,到底是詐尸還是另有隱情格嗅,我是刑警寧澤番挺,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站屯掖,受9級(jí)特大地震影響玄柏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜贴铜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一禁荸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧阀湿,春花似錦、人聲如沸瑰妄。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)间坐。三九已至灾挨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間竹宋,已是汗流浹背劳澄。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蜈七,地道東北人秒拔。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像飒硅,于是被迫代替她去往敵國(guó)和親砂缩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350

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