NSNotification 線程管理以及自動註銷開源方案

背景

ios 的 notification 在多線程的狀況下,線程的管理很是很差控制。這個怎麼理解呢?html

按照官方文檔的說法就是,無論你在哪一個線程註冊了 observer,notification 在哪一個線程 post,那麼它就將在哪一個線程接收,這個意思用代碼表示,效果以下:ios

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"current thread = %@", [NSThread currentThread]);
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:POST_NOTIFICATION object:nil];
    
}

- (void)viewDidAppear:(BOOL)animated {
    [self postNotificationInBackground];
}

- (void)postNotificationInBackground {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        [[NSNotificationCenter defaultCenter] postNotificationName:POST_NOTIFICATION object:nil userInfo:nil];
    });
}

- (void)handleNotification:(NSNotification *)notification {
     NSLog(@"current thread = %@", [NSThread currentThread]);
}

輸出以下:git

2016-07-02 11:20:56.683 Test[31784:3602420] current thread = <NSThread: 0x7f8548405250>{number = 1, name = main}
2016-07-02 11:20:56.684 Test[31784:3602420] viewWillAppear: ViewController
2016-07-02 11:20:56.689 Test[31784:3602469] current thread = <NSThread: 0x7f854845b790>{number = 2, name = (null)}

也就是說,儘管我在主線程註冊了 observer,可是因爲我在子線程 post 了消息,那麼 handleNotification 響應函數也會在子線程處理。這樣一來就會給咱們帶來困擾,由於 notification 的響應函數執行線程將變得不肯定,並且不少操做如 UI 操做,咱們是須要在主線程進行的。github

解決方案

怎麼解決這個問題呢?api

在響應函數處強制切線程

實現

一個很土的方法就在在 handleNotification 裏面,強制切換線程,如:數組

- (void)handleNotification:(NSNotification *)notification {
   dispatch_async(dispatch_get_main_queue(), ^{
      NSLog(@"current thread = %@", [NSThread currentThread]);
   });
}
缺陷

每個響應函數都強制切換線程。這樣帶來的問題就是每一處理代碼你都得這樣作,對於開發者而言負擔太大,顯然是下下策。多線程

線程重定向

其實解決思路和上面的差很少,不過實現的方式更優雅一點,這個方案在 apple 的官方文檔中有詳細介紹,它的思路翻譯過來就是:重定向通知的一種的實現思路是使用一個通知隊列(注意,不是 NSNotificationQueue 對象,而是一個數組)去記錄全部的被拋向非預期線程裏面的通知,而後將它們重定向到預期線程。這種方案使咱們仍然是像日常同樣去註冊一個通知的觀察者,當接收到 Notification 的時候,先判斷 post 出來的這個 Notification 的線程是否是咱們所指望的線程,若是不是,則將這個 Notification 存儲到咱們自定義的隊列中,併發送一個信號( signal )到指望的線程中,來告訴這個線程須要處理一個 Notification 。指定的線程在收到信號後,將 Notification 從隊列中移除,並進行處理。併發

實現
/* Threaded notification support. */
@property (nonatomic) NSMutableArray    *notifications;         // 通知隊列
@property (nonatomic) NSThread          *notificationThread;    // 預想的處理通知的線程
@property (nonatomic) NSLock            *notificationLock;      // 用於對通知隊列加鎖的鎖對象,避免線程衝突
@property (nonatomic) NSMachPort        *notificationPort;      // 用於向預想的處理線程發送信號的通訊端口

@end

@implementation ViewController

 - (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"current thread = %@", [NSThread currentThread]);
    
    [self setUpThreadingSupport];
    
    // 往當前線程的run loop添加端口源
    // 當Mach消息到達而接收線程的run loop沒有運行時,則內核會保存這條消息,直到下一次進入run loop
    [[NSRunLoop currentRunLoop] addPort:self.notificationPort
                                forMode:(__bridge NSString *)kCFRunLoopCommonModes];
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:POST_NOTIFICATION object:nil];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        [[NSNotificationCenter defaultCenter] postNotificationName:POST_NOTIFICATION object:nil userInfo:nil];
        
    });
}

 - (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 {
    
    if ([NSThread currentThread] != _notificationThread) {
        // Forward the notification to the correct thread.
        [self.notificationLock lock];
        [self.notifications addObject:notification];
        [self.notificationLock unlock];
        [self.notificationPort sendBeforeDate:[NSDate date]
                                   components:nil
                                         from:nil
                                     reserved:0];
    }
    else {
        // Process the notification here;
        NSLog(@"current thread = %@", [NSThread currentThread]);
        NSLog(@"process notification");
    }
}
}

可是這種方案有明顯額缺陷,官方文檔也對其進行了說明,歸結起來有兩點:app

  • 全部的通知的處理都要通過 processNotification 函數進行處理。async

  • 全部的接聽對象都要提供相應的 NSMachPort 對象,進行消息轉發。

正是因爲存在這樣的缺陷,所以官方文檔並不建議直接這樣使用,而是鼓勵開發者去繼承NSNoticationCenter 或者本身去提供一個單獨的類進行線程的維護。

block 方式的 NSNotification

爲了順應語法的變化,apple 從 ios4 以後提供了帶有 block 的 NSNotification。使用方式以下:

- (id<NSObject>)addObserverForName:(NSString *)name
                            object:(id)obj
                             queue:(NSOperationQueue *)queue
                        usingBlock:(void (^)(NSNotification *note))block

這裏說明幾點

  • 觀察者就是當前對象

  • queue 定義了 block 執行的線程,nil 則表示 block 的執行線程和發通知在同一個線程

  • block 就是相應通知的處理函數

這個 API 已經可以讓咱們方便的控制通知的線程切換。可是,這裏有個問題須要注意。就是其 remove 操做。

首先回憶一下咱們原來的 NSNotification 的 remove 方式,見以下代碼:

- (void)removeObservers {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:POST_NOTIFICATION object:nil];
}

須要指定 observer 以及 name。可是帶 block 方式的 remove 便不能像上面這樣處理了。其方式以下:

- (void)removeObservers {
    if(_observer){
        [[NSNotificationCenter defaultCenter] removeObserver:_observer];
    }
}

其中 _observer 是 addObserverForName 方式的 api 返回觀察者對象。這也就意味着,你須要爲每個觀察者記錄一個成員對象,而後在 remove 的時候依次刪除。試想一下,你若是須要 10 個觀察者,則須要記錄 10 個成員對象,這個想一想就是很麻煩,並且它還不可以方便的指定 observer 。所以,理想的作法就是本身再作一層封裝,將這些細節封裝起來。

LRNotificationObserver

git 上有一個想要解決上述問題的開源代碼,其使用方式以下:

+ (void)observeName:(NSString *)name
              owner:(id)owner
      dispatchQueue:(dispatch_queue_t)dispatchQueue
              block:(LRNotificationObserverBlock)block;

它可以方便的控制線程切換,並且它還能作到 owner dealloc 的時候,自動 remove observer。好比咱們不少時候在 viewDidLoad 的時候addObserver,而後還須要重載 dealloc,在裏面調用 removeObserver,這個開源方案,幫咱們省去了再去dealloc 顯示 remove 的額外工做。可是若是你想顯式的調用 remove,就比較麻煩了(好比有時候,咱們在viewWillAppear 添加了 observer,須要在 viewWillDisAppear 移除 observer),它相似官方的解決方案,須要你用成員變量,將 observer 一個個保存下來,而後在 remove 的地方移除。

GYNotificationCenter

爲了解決上面的問題,所以決定從新寫一個 Notification 的管理類,GYNotificationCenter 想要達到的效果有兩個

  1. 可以方便的控制線程切換

  2. 可以方便的remove observer

使用

- (void)addObserver:(nonnull id)observer
              name:(nonnull NSString *)aName
     dispatchQueue:(nullable dispatch_queue_t)disPatchQueue
             block:(nonnull GYNotificatioObserverBlock)block;

咱們提供了和官方 api 幾乎同樣的調用方法,支持傳入 dispatchQueue 實現線程切換控制,同時可以以 block 的方式處理消息響應,並且支持在 observer dealloc 的時候,自動調用 observer 的 remove 操做。同時還提供了和原生同樣的顯式調用 remove 的操做,方便收到調用 remove .

- (void)removerObserver:(nonnull id)observer
                   name:(nonnull NSString *)anName
                 object:(nullable id)anObject;

- (void)removerObserver:(nonnull id)observer;

可以方便的手動調用 remove 操做。

實現思路

GYNotificaionCenter 借鑑了官方的線程重定向 以及 LRNotificationObserver 的一些方案。在 addObserver 的時候,生成了一個和 observer 關聯的 GYNotificationOberverIdentifer 對象,這個對象記錄了傳入的 block 、name 的數據,而後對這個對象依據傳入的 name 註冊觀察者。

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:anName object:object];

當收到通知的時候,在 handleNotification 裏面執行傳入的 block,回調的外面去。

- (void)handleNotification:(NSNotification *)notification {
    
    if (self.dispatchQueue) {
        dispatch_async(self.dispatchQueue, ^{
            if (self.block) {
                self.block(notification);
            }
        });
    } else {
        self.block(notification);
    }   
}

GYNotificationOberverIdentifer 對象放入 GYNotificationOberverIdentifersContainer 對象中進行統一管理。

- (void)addNotificationOberverIdentifer:(GYNotificationOberverIdentifer *)identifier {
    
    NSAssert(identifier,@"identifier is nil");
    if (identifier) {
        NotificationPerformLocked(^{
            [self modifyContainer:^(NSMutableDictionary *notificationOberverIdentifersDic) {
                //不重複add observer
                if (![notificationOberverIdentifersDic objectForKey:identifier.name]) {
                    [notificationOberverIdentifersDic setObject:identifier forKey:identifier.name];
                }
            }];
        });
    }
    
}

這個對象也和 observer 關聯。因爲其和 observer 是關聯的,所以當 observer 釋放的時候,GYNotificationOberverIdentifer 也會釋放,所以,也就能在 GYNotificationOberverIdentifer 的 dealloc 裏面調用 remove 操做移除通知註冊從而實現自動 remove。

同時因爲 GYNotificationOberverIdentifersContainer 裏面保留了全部的 Identifer 對象,所以也就可以方便的根據 name 進行 remove 了。

- (void)removeObserverWithName:(NSString *)name {
    
    if (name) {
        NotificationPerformLocked(^{
            [self modifyContainer:^(NSMutableDictionary *notificationOberverIdentifersDic) {
                
                if ([notificationOberverIdentifersDic objectForKey:name]) {
                    GYNotificationOberverIdentifer *identifier = (GYNotificationOberverIdentifer *)[notificationOberverIdentifersDic objectForKey:name];
                    [identifier stopObserver];
                    [notificationOberverIdentifersDic removeObjectForKey:name];
                }
            }];

        });
    }
}
相關文章
相關標籤/搜索