iOS進階之NSNotification的實現原理

一、NSNotification使用

1、向觀察者中心添加觀察者:

  • 方式一:觀察者接收到通知后執行任務的代碼在發送通知的線程中執行
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;

  • 方式二:觀察者接受到通知后執行任務的代碼在指定的操作隊列中執行
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block

2、通知中心向觀察者發送消息

- (void)postNotification:(NSNotification *)notification;

- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;

- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;

3、移除觀察者

- (void)removeObserver:(id)observer;
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;

二、實現原理

1、首先了解Observation、NCTable這個結構體內部結構

當你調用addObserver:selector:name:object:會創建一個Observation,Observation的結構如下代碼:

typedef struct  Obs {
  id observer;  //接受消息的對象   
  SEL selector; //執行的方法
  struct Obs    *next;  //下一Obs節點指針
  int   retained;   //引用計數
  struct NCTbl *link; //執向chunk table指針
} Observation;

對于Observation持有observer:

  • 在iOS9以前:

    • 持有的是一個__unsafe_unretain指針對象,當對象釋放時,會訪問已經釋放的對象,造成BAD_ACCESS。
    • 在iOS9之后:持有的是weak類型指針,當observer釋放時observer會置nil,nil對象performSelector不再會崩潰。
  • name和Observation是映射關系。

    • observer和sel包含在Observation結構體中。

Observation對象存在哪?

NSNotification維護了全局對象表NCTable結構,結構體里包含GSIMapTable表的結構,用于存儲Observation。代碼如下:

#define CHUNKSIZE   128
#define CACHESIZE   16
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;

數據結構重要的參數:

  • wildcard:保存既沒有通知名稱又沒有傳入object的通知單鏈表;
  • nameless:存儲沒有傳入名字的通知名稱的hash表。
  • named:存儲傳入了名字的通知的hash表。
  • cache:用于快速緩存.

這里值得注意nameless和named的結構,雖然都是hash表,存儲的東西還有點區別:

  • nameless表中的GSIMapTable的結構如下
key value
object Observation
object Observation
object Observation

沒有傳入名字的nameless表,key就是object參數,vaule為Observation結構體

  • 在named表中GSIMapTable結構如下:
key value
name maptable
name maptable
name maptable
  • maptable也是一個hash表,結構如下:
key value
object Observation
object Observation
object Observation

傳入名字的通知是存放在叫named的hash表
kay為name,value還是maptable也是一個hash表
maptable表的key為object參數,value為Observation參數

2、addObserver:selector:name:object: 方法內部實現原理

- (void) addObserver: (id)observer
            selector: (SEL)selector
                name: (NSString*)name
              object: (id)object
{
    Observation *list;
    Observation *o;
    GSIMapTable m;
    GSIMapNode  n;

    //入參檢查異常處理
    ...
        //table加鎖保持數據一致性,同一個線程按順序執行,是同步的
    lockNCTable(TABLE);
        //創建Observation對象包裝相應的調用函數
    o = obsNew(TABLE, selector, observer);
        //處理存在通知名稱的情況
    if (name)
    {
        //table表中獲取相應name的節點
        n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
        if (n == 0)
        {
           //未找到相應的節點,則創建內部GSIMapTable表,以name作為key添加到talbe中
          m = mapNew(TABLE);
          name = [name copyWithZone: NSDefaultMallocZone()];
          GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
          GS_CONSUMED(name)
        }
        else
        {
            //找到則直接獲取相應的內部table
            m = (GSIMapTable)n->value.ptr;
        }

        //內部table表中獲取相應object對象作為key的節點
        n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
        if (n == 0)
        {
            //不存在此節點,則直接添加observer對象到table中
            o->next = ENDOBS;//單鏈表observer末尾指向ENDOBS
            GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
        }
        else
        {
            //存在此節點,則獲取并將obsever添加到單鏈表observer中
            list = (Observation*)n->value.ptr;
            o->next = list->next;
            list->next = o;
        }
    }
    //只有觀察者對象情況
    else if (object)
    {
        //獲取對應object的table
        n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
        if (n == 0)
        {
            //未找到對應object key的節點,則直接添加observergnustep-base-1.25.0
            o->next = ENDOBS;
            GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
        }
        else
        {
            //找到相應的節點則直接添加到鏈表中
            list = (Observation*)n->value.ptr;
            o->next = list->next;
            list->next = o;
        }
    }
    //處理即沒有通知名稱也沒有觀察者對象的情況
    else
    {
        //添加到單鏈表中
        o->next = WILDCARD;
        WILDCARD = o;
    }
        //解鎖
    unlockNCTable(TABLE);
}

添加通知的基本邏輯:

  1. 根據傳入的selector和observer創建Observation,并存入GSIMaptable中,如果已存在,則是從cache中取。

  2. 如果name存在:

    • 則向named表中插入元素,key為name,value為GSIMaptable。
    • GSIMaptable里面key為object,value為Observation,結束
  3. 如果name不存在:

    • 則向nameless表中插入元素,key為object,value為Observation,結束
  4. 如果name和object都不存在,則把這個Observation添加WILDCARD鏈表中

三、addObserverForName:object:queueusingBlock:實現原理

//對于block形式,里面創建了GSNotificationObserver對象,然后在調用addObserver: selector: name: object:
- (id) addObserverForName: (NSString *)name 
                   object: (id)object 
                    queue: (NSOperationQueue *)queue 
               usingBlock: (GSNotificationBlock)block
{
    GSNotificationObserver *observer = 
        [[GSNotificationObserver alloc] initWithQueue: queue block: block];

    [self addObserver: observer 
             selector: @selector(didReceiveNotification:) 
                 name: name 
               object: object];

    return observer;
}

/*
1.初始化該隊列會創建Block_copy 拷貝block
2.并確定通知操作隊列
*/
- (id) initWithQueue: (NSOperationQueue *)queue 
               block: (GSNotificationBlock)block
{
    self = [super init];
    if (self == nil)
        return nil;

    ASSIGN(_queue, queue);
    _block = Block_copy(block);
    return self;
}

/*
1.通知的接受處理函數didReceiveNotification,
2.如果queue不為空,通過addOperation來實現指定操作隊列處理
3.如果queue不為空,直接在當前線程執行block。
*/
- (void) didReceiveNotification: (NSNotification *)notif
{
    if (_queue != nil)
    {
        GSNotificationBlockOperation *op = [[GSNotificationBlockOperation alloc] 
            initWithNotification: notif block: _block];

        [_queue addOperation: op];
    }
    else
    {
        CALL_BLOCK(_block, notif);
    }
}

4、發送通知的實現 postNotificationName: name: object:

 - (void) _postAndRelease: (NSNotification*)notification
{
    1.入參檢查校驗
    2.創建存儲所有匹配通知的數組GSIArray
    3.加鎖table避免數據一致性問題
    4.查找既不監聽name也不監聽object所有的wildcard類型的Observation,加入數組GSIArray中
    5.查找NAMELESS表中指定對應觀察者對象object的Observation并添加到數組中
    6.查找NAMED表中相應的Observation并添加到數組中
        1. 首先查找name與object的一致的Observation加入數組中
        2. 當object為nil的Observation加入數組中
    //解鎖table
    //遍歷整個數組并依次調用performSelector:withObject處理通知消息發送
    //解鎖table并釋放資源
}

二、NSNotification相關問題

1、對于addObserver方法,為什么需要object參數?

  1. addObserver當你不傳入name也可以,傳入object,當postNotification方法同樣發出這個object時,就會觸發通知方法。

因為當name不存在的時候,會繼續判斷object,則向nameless的maptable表中插入元素,key為object,value為Observation

2、都傳入null對象會怎么樣

你可能也注意到了,addObserver方法name和object都可以為空,這表示將會把observer賦值為 wildcard,他將會監聽所有的通知。

3、通知的發送時同步的,還是異步的。

同步異步這個問題,由于TABLE資源的問題,同一個線程會按順序遍歷數組執行,自然是同步的。

4、NSNotificationCenter接受消息和發送消息是在一個線程里嗎?如何異步發送消息

由于是使用的performSelector方法,沒有進行轉線程,默認是postNotification方法的線程。

[o->observer performSelector: o->selector 
withObject: notification];

對于異步發送消息,可以使用NSNotificationQueue,queue顧明意思,我們是需要將NSNotification放入queue中執行的。

NSNotificationQueue發送消息的三種模式:

typedef NS_ENUM(NSUInteger, NSPostingStyle) {
    NSPostWhenIdle = 1, // 當runloop處于空閑狀態時post
    NSPostASAP = 2, // 當當前runloop完成之后立即post
    NSPostNow = 3  // 立即post
};

NSNotification *notification = [NSNotification notificationWithName:@"111" object:nil];
[[NSNotificationQueue defaultQueue] enqueueNotification: notification postingStyle:NSPostASAP];
  • NSPostingStyle為NSPostNow 模式是同步發送,
  • NSPostWhenIdle或者NSPostASAP是異步發送

5、NSNotificationQueue和runloop的關系?

NSNotificationQueue 是依賴runloop才能成功觸發通知,如果去掉runloop的方法,你會發現無法觸發通知。

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    //子線程的runloop需要自己主動開啟     
   NSNotification *notification = [NSNotification notificationWithName:@"TEST" object:nil];
        [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostWhenIdle coalesceMask:NSNotificationNoCoalescing forModes:@[NSDefaultRunLoopMode]];
        // run runloop
        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSRunLoopCommonModes];
        CFRunLoopRun();
        NSLog(@"3");
    });

NSNotification *notification = [NSNotification notificationWithName:@"111" object:nil];
[[NSNotificationQueue defaultQueue] enqueueNotification: notification postingStyle:NSPostASAP];

NSNotificationQueue將通知添加到隊列中時,其中postringStyle參數就是定義通知調用和runloop狀態之間關系。

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

  1. 保證主線程發送消息或者接受消息方法里切換到主線程

  2. 接收到通知后跳轉到主線程,蘋果建議使用NSMachPort進行消息轉發到主線程。

實現代碼如下:

machPort轉發到主線程_1.png
machPort轉發到主線程_2.png
machPort轉發到主線程_3.png
machPort轉發到主線程_4.png
machPort轉發到主線程_5.png
  1. 使用block接口addObserverForName:object:queue:usingBlock:指定主線程

7、頁面銷毀時不移除通知會崩潰嗎?

在iOS9之前會,iOS9之后不會

對于Observation持有observer

在iOS9之前:不是一個類似OC中的weak類型,持有的相當與一個__unsafe_unretain指針對象,當對象釋放時,會訪問已經釋放的對象,造成BAD_ACCESS。
在iOS9之后:持有的是weak類型指針,對nil對象performSelector不再會崩潰

8、多次添加同一個通知會是什么結果?多次移除通知呢?

  1. 由于源碼中并不會進行重復過濾,所以添加同一個通知,等于就是添加了2次,回調也會觸發兩次。

  2. 關于多次移除,并沒有問題,因為會去map中查找,找到才會刪除。當name和object都為nil時,會移除所有關于該observer的WILDCARD

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

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];

[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];

根據postNotification的實現:

  • 會找到key為TestNotification的maptable,
  • 再從中選擇key為nil的observation,
  • 所以是找不到以@1為key的observation的
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,484評論 2 379

推薦閱讀更多精彩內容