NSNotificationCenter

一個NSNotificationCenter對象(通知中心)提供了在程序中廣播消息的機制,它實質上就是一個通知分發表。這個分發表負責維護爲各個通知註冊的觀察者,並在通知到達時,去查找相應的觀察者,將通知轉發給他們進行處理。html

本文主要了整理了一下NSNotificationCenter的使用及須要注意的一些問題,並提出了一些未解決的問題,但願能在此獲得解答。安全

獲取通知中心

每一個程序都會有一個默認的通知中心。爲此,NSNotificationCenter提供了一個類方法來獲取這個通知中心:多線程

+ (NSNotificationCenter *)defaultCenter

獲取了這個默認的通知中心對象後,咱們就可使用它來處理通知相關的操做了,包括註冊觀察者,移除觀察者、發送通知等。併發

一般若是不是出於必要,咱們通常都使用這個默認的通知中心,而不本身建立維護一個通知中心。app

添加觀察者

若是想讓對象監聽某個通知,則須要在通知中心中將這個對象註冊爲通知的觀察者。早先,NSNotificationCenter提供瞭如下方法來添加觀察者:框架

- (void)addObserver:(id)notificationObserver
           selector:(SEL)notificationSelector
               name:(NSString *)notificationName
             object:(id)notificationSender

這個方法帶有4個參數,分別指定了通知的觀察者、處理通知的回調、通知名及通知的發送對象。這裏須要注意幾個問題:async

  1. notificationObserver不能爲nil。
  2. notificationSelector回調方法有且只有一個參數(NSNotification對象)。
  3. 若是notificationName爲nil,則會接收全部的通知(若是notificationSender不爲空,則接收全部來自於notificationSender的全部通知)。如代碼清單1所示。
  4. 若是notificationSender爲nil,則會接收全部notificationName定義的通知;不然,接收由notificationSender發送的通知。
  5. 監聽同一條通知的多個觀察者,在通知到達時,它們執行回調的順序是不肯定的,因此咱們不能去假設操做的執行會按照添加觀察者的順序來執行。

對於以上幾點,咱們來重點關注一下第3條。如下代碼演示了當咱們的notificationName設置爲nil時,通知的監聽狀況。post

代碼清單1:添加一個Observer,其中notificationName爲nil性能

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

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

    [[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];
}

- (void)handleNotification:(NSNotification *)notification
{
    NSLog(@"notification = %@", notification.name);
}

@end

運行後的輸出結果以下:測試

notification = TestNotification
notification = UIWindowDidBecomeVisibleNotification
notification = UIWindowDidBecomeKeyNotification
notification = UIApplicationDidFinishLaunchingNotification
notification = _UIWindowContentWillRotateNotification
notification = _UIApplicationWillAddDeactivationReasonNotification
notification = _UIApplicationDidRemoveDeactivationReasonNotification
notification = UIDeviceOrientationDidChangeNotification
notification = _UIApplicationDidRemoveDeactivationReasonNotification
notification = UIApplicationDidBecomeActiveNotification

能夠看出,咱們的對象基本上監聽了測試程序啓動後的所示消息。固然,咱們不多會去這麼作。

而對於第4條,使用得比較多的場景是監聽UITextField的修改事件,一般咱們在一個ViewController中,只但願去監聽當前視圖中的UITextField修改事件,而不但願監聽全部UITextField的修改事件,這時咱們就能夠將當前頁面的UITextField對象指定爲notificationSender。

在iOS 4.0以後,NSNotificationCenter爲了跟上時代,又提供了一個以block方式實現的添加觀察者的方法,以下所示:

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

你們第一次看到這個方法時是否會有這樣的疑問:觀察者呢?參數中並無指定具體的觀察者,那誰是觀察者呢?實際上,與前一個方法不一樣的是,前者使用一個現存的對象做爲觀察者,而這個方法會建立一個匿名的對象做爲觀察者(即方法返回的id<NSObject>對象),這個匿名對象會在指定的隊列(queue)上去執行咱們的block。

這個方法的優勢在於添加觀察者的操做與回調處理操做的代碼更加緊湊,不須要拼命滾動鼠標就能直接找處處理代碼,簡單直觀。這個方法也有幾個地方須要注意:

  1. name和obj爲nil時的情形與前面一個方法是相同的。
  2. 若是queue爲nil,則消息是默認在post線程中同步處理,即通知的post與轉發是在同一線程中;但若是咱們指定了操做隊列,狀況就變得有點意思了,咱們一會再講。
  3. block塊會被通知中心拷貝一份(執行copy操做),以在堆中維護一個block對象,直到觀察者被從通知中心中移除。因此,應該特別注意在block中使用外部對象,避免出現對象的循環引用,這個咱們在下面將舉例說明。
  4. 若是一個給定的通知觸發了多個觀察者的block操做,則這些操做會在各自的Operation Queue中被併發執行。因此咱們不能去假設操做的執行會按照添加觀察者的順序來執行。
  5. 該方法會返回一個表示觀察者的對象,記得在不用時釋放這個對象。

下面咱們重點說明一下第2點和第3點。

關於第2點,當咱們指定一個Operation Queue時,無論通知是在哪一個線程中post的,都會在Operation Queue所屬的線程中進行轉發,如代碼清單2所示:

代碼清單2:在指定隊列中接收通知

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [[NSNotificationCenter defaultCenter] addObserverForName:TEST_NOTIFICATION object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {

        NSLog(@"receive thread = %@", [NSThread currentThread]);
    }];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        NSLog(@"post thread = %@", [NSThread currentThread]);
        [[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];
    });
}

@end

在這裏,咱們在主線程裏添加了一個觀察者,並指定在主線程隊列中去接收處理這個通知。而後咱們在一個全局隊列中post了一個通知。咱們來看下輸出結果:

post thread = <NSThread: 0x7ffe0351f5f0>{number = 2, name = (null)}
receive thread = <NSThread: 0x7ffe03508b30>{number = 1, name = main}

能夠看到,消息的post與接收處理並非在同一個線程中。如上面所提到的,若是queue爲nil,則消息是默認在post線程中同步處理,你們能夠試一下。

對於第3點,因爲使用的是block,因此須要注意的就是避免引發循環引用的問題,如代碼清單3所示:

代碼清單3:block引起的循環引用問題

@interface Observer : NSObject

@property (nonatomic, assign) NSInteger i;
@property (nonatomic, weak) id<NSObject> observer;

@end

@implementation Observer

- (instancetype)init
{
    self = [super init];

    if (self)
    {
        NSLog(@"Init Observer");

        // 添加觀察者
        _observer =  [[NSNotificationCenter defaultCenter] addObserverForName:TEST_NOTIFICATION object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {

            NSLog(@"handle notification");

            // 使用self
            self.i = 10;
        }];
    }

    return self;
}

@end

#pragma mark - ViewController

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self createObserver];

    // 發送消息
    [[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];
}

- (void)createObserver {

    Observer *observer = [[Observer alloc] init];
}

@end

運行後的輸出以下:

Init Observer
handle notification

咱們能夠看到createObserver中建立的observer並無被釋放。因此,使用 – addObserverForName:object:queue:usingBlock:必定要注意這個問題。

移除觀察者

與註冊觀察者相對應的,NSNotificationCenter爲咱們提供了兩個移除觀察者的方法。它們的定義以下:

- (void)removeObserver:(id)notificationObserver

- (void)removeObserver:(id)notificationObserver name:(NSString *)notificationName object:(id)notificationSender

前一個方法會將notificationObserver從通知中心中移除,這樣notificationObserver就沒法再監放任何消息。然後一個會根據三個參數來移除相應的觀察者。

這兩個方法也有幾點須要注意:

  1. 因爲註冊觀察者時(不論是哪一個方法),通知中心會維護一個觀察者的弱引用,因此在釋放對象時,要確保移除對象全部監聽的通知。不然,可能會致使程序崩潰或一些莫名其妙的問題。
  2. 對於第二個方法,若是notificationName爲nil,則會移除全部匹配notificationObserver和notificationSender的通知,同理notificationSender也是同樣的。而若是notificationName和notificationSender都爲nil,則其效果就與第一個方法是同樣的了。你們能夠試一下。
  3. 最有趣的應該是這兩個方法的使用時機。–removeObserver:適合於在類的dealloc方法中調用,這樣能夠確保將對象從通知中心中清除;而在viewWillDisappear:這樣的方法中,則適合於使用-removeObserver:name:object:方法,以免不知情的狀況下移除了不該該移除的通知觀察者。例如,假設咱們的ViewController繼承自一個類庫的某個ViewController類(假設爲SKViewController吧),可能SKViewController自身也監聽了某些通知以執行特定的操做,但咱們使用時並不知道。若是直接在viewWillDisappear:中調用–removeObserver:,則也會把父類監聽的通知也給移除。

關於註冊監聽者,還有一個須要注意的問題是,每次調用addObserver時,都會在通知中心從新註冊一次,即便是同一對象監聽同一個消息,而不是去覆蓋原來的監聽。這樣,當通知中心轉發某一消息時,若是同一對象屢次註冊了這個通知的觀察者,則會收到多個通知,如代碼清單4所示:

代碼清單4:同一對象屢次註冊同一消息

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

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

    [[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];
}

- (void)handleNotification:(NSNotification *)notification
{
    NSLog(@"notification = %@", notification.name);
}

@end

其輸出結果以下所示:

notification = TestNotification
notification = TestNotification

能夠看到對象處理了兩次通知。因此,若是咱們須要在viewWillAppear監聽一個通知時,必定要記得在對應的viewWillDisappear裏面將觀察者移除,不然就可能會出現上面的狀況。

最後,再特別重點強調的很是重要的一點是,在釋放對象前,必定要記住若是它監聽了通知,必定要將它從通知中心移除。若是是用 – addObserverForName:object:queue:usingBlock:,也記得必定得移除這個匿名觀察者。說白了就一句話,添加和移除要配對出現。

post消息

註冊了通知觀察者,咱們即可以隨時隨地的去post一個通知了(固然,若是閒着沒事,也能夠不註冊觀察者,post通知隨便玩,只是沒人理睬罷了)。NSNotificationCenter提供了三個方法來post一個通知,以下所示:

- postNotification:
– postNotificationName:object:
– postNotificationName:object:userInfo:

咱們能夠根據須要指定通知的發送者(object)並附帶一些與通知相關的信息(userInfo),固然這些發送者和userInfo能夠封裝在一個NSNotification對象中,由- postNotification:來發送。注意,- postNotification:的參數不能爲空,不然會引起一個異常,以下所示:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSNotificationCenter postNotification:]: notification is nil'

每次post一個通知時,通知中心都會去遍歷一下它的分發表,而後將通知轉發給相應的觀察者。

另外,通知的發送與處理是同步的,在某個地方post一個消息時,會等到全部觀察者對象執行完處理操做後,纔回到post的地方,繼續執行後面的代碼。如代碼清單5所示:

代碼清單5:通知的同步處理

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

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

    [[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];

    NSLog(@"continue");
}

- (void)handleNotification:(NSNotification *)notification
{
    NSLog(@"handle notification");
}

@end

運行後輸出結果是:

handle notification
continue

一些思考

翻了好些資料,還有兩個問題始終沒有明確的答案。

首先就是通知中心是如何維護觀察者對象的。能夠明確的是,添加觀察者時,通知中心沒有對觀察者作retain操做,即不會使觀察者的引用計數加1。那通知中心維護的是觀察者的weak引用呢仍是unsafe_unretained引用呢?

我的認爲多是unsafe_unretained的引用,由於咱們知道若是是weak引用,其所指的對象被釋放後,這個引用會被置成nil。而實際狀況是通知中心還會給這個對象發送消息,並引起一個異常。而若是向nil發送一個消息是不會致使異常的。

【很是感謝 @lv-pw,上面這個問題在《斯坦福大學公開課:iOS 7應用開發》的第5集的第57分50秒中獲得瞭解答:確實使用的是unsafe_unretained,老師的解釋是,之因此使用unsafe_unretained,而不使用weak,是爲了兼容老版本的系統。】

另外,咱們知道NSNotificationCenter實現的是觀察者模式,並且一般狀況下消息在哪一個線程被post,就在哪一個線程被轉發。而從上面的描述能夠發現, -addObserverForName:object:queue:usingBlock:添加的匿名觀察者能夠在指定的隊列中處理通知,那它的實現機制是什麼呢?

小結

在咱們的應用程序中,一個大的話題就是兩個對象之間如何通訊。咱們須要根據對象之間的關係來肯定採用哪種通訊方式。對象之間的通訊方式主要有如下幾種:

  1. 直接方法調用
  2. Target-Action
  3. Delegate
  4. 回調(block)
  5. KVO
  6. 通知

通常狀況下,咱們能夠根據如下兩點來肯定使用哪一種方式:

  1. 通訊對象是一對一的仍是一對多的
  2. 對象之間的耦合度,是強耦合仍是鬆耦合

Objective-C中的通知因爲其廣播性及鬆耦合性,很是適合於大的範圍內對象之間的通訊(模塊與模塊,或一些框架層級)。通知使用起來很是方便,也正由於如此,因此容易致使濫用。因此在使用前仍是須要多想一想,是否有更好的方法來實現咱們所須要的對象間通訊。畢竟,通知機制會在必定程度上會影響到程序的性能。

對於使用NSNotificationCenter,最後總結一些小建議:

  1. 在須要的地方使用通知。
  2. 註冊的觀察者在不使用時必定要記得移除,即添加和移除要配對出現。
  3. 儘量遲地去註冊一個觀察者,並儘量早將其移除,這樣能夠改善程序的性能。由於,每post一個通知,都會是遍歷通知中心的分發表,確保通知發給每個觀察者。
  4. 記住通知的發送和處理是在同一個線程中。
  5. 使用-addObserverForName:object:queue:usingBlock:務必處理好內存問題,避免出現循環引用。
  6. NSNotificationCenter是線程安全的,但並不意味着在多線程環境中不須要關注線程安全問題。不恰當的使用仍然會引起線程問題。
相關文章
相關標籤/搜索