NSNotification,看完你就都懂了

什么?你以為你通知全懂了?抱歉,你懂的僅僅是基礎。

一、為什么要使用NSNotification

The standard way to pass information between objects is message passing—one object invokes the method of another object. However, message passing requires that the object sending the message know who the receiver is and what messages it responds to. At times, this tight coupling of two objects is undesirable—most notably because it would join together two otherwise independent subsystems. For these cases, a broadcast model is introduced: An object posts a notification, which is dispatched to the appropriate observers through an NSNotificationCenter object, or simply notification center.

大概意思是,若想objectA需要調用objectB的一個方法,一般來說,需要objectA中能訪問到objectB,但是這是一種不受歡迎的會造成耦合的方式。所以,引入了一個新機制,叫NSNotificationCenter,由NSNotificationCenter管理objectB等注冊的object,而objectA不需要知道objectB,只需要通過標記(例如NotificationName)在NSNotificationCenter中找到對應的object,從而調用該object的方法。無疑,這是一種松耦合,并且允許多對多的一種方式。

通知和delegate的基本區別:

  • 通知是允許多對多的,而delegate只能是1對1的。
  • 通知是松耦合的,通知方不需要知道被通知方的任何情況,而delegate不行。
  • 通知的效率比起delegate略差。

二、通知的基本使用

1、通知中的基本概念

NSNotification

// NSNotification.h相關代碼
@interface NSNotification : NSObject <NSCopying, NSCoding>
@property (readonly, copy) NSNotificationName name;
@property (nullable, readonly, retain) id object;
@property (nullable, readonly, copy) NSDictionary *userInfo;
- (instancetype)initWithName:(NSNotificationName)name object:(nullable id)object userInfo:(nullable NSDictionary *)userInfo NS_AVAILABLE(10_6, 4_0) NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
@end
@interface NSNotification (NSNotificationCreation)
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject;
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
- (instancetype)init /*NS_UNAVAILABLE*/;    /* do not invoke; not a valid initializer for this class */
@end

// 理解
NSNotification作為一種消息傳遞的媒介,包含三個public成員變量,通過NSNotificationName類型的name來查找對應observer,并且可以在object和userInfo中傳入參數。可以使用上述的幾種初始化方式進行初始化。

NSNotificationCenter

// 默認的通知中心,全局只有一個
@property (class, readonly, strong) NSNotificationCenter *defaultCenter;
// 在通知中心添加觀察者
- (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;
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;
// iOS4以后,以block的形式代替selector方式為通知中心添加觀察者
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block NS_AVAILABLE(10_6, 4_0);

** NSDistributedNotificationCenter**

// 在iOS的框架中找不到這個類相關知識。該通知中心是Mac OS中,進程間通知使用。不多進行介紹。

2、通知的基本使用

代碼

// 代碼
#import "AppDelegate.h"
@interface A : NSObject
- (void)test;
@end
@implementation A
static int count = 0;
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)test {
    // 觀察方式A:selector方式
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(xxx) name:@"111" object:nil];
    // 觀察方式B:block方式(queue參數決定你想把該block在哪一個NSOperationQueue里面執行)
    [[NSNotificationCenter defaultCenter] addObserverForName:@"111" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
        NSLog(@"block %d", ++count);
    }];
}
- (void)xxx {
    NSLog(@"selector %d", ++count);
}
@end
@interface AppDelegate ()
@property (nonatomic, strong) A *a;
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    A *a = [A new];
    [a test];
    self.a = a;
    // 發送方式A:手動定義NSNotification
    NSNotification *noti = [NSNotification notificationWithName:@"111" object:nil];
    [[NSNotificationCenter defaultCenter] postNotification:noti];
    // 發送方式B:自動定義NSNotification
    [[NSNotificationCenter defaultCenter] postNotificationName:@"111" object:nil userInfo:nil];
    return YES;
}
@end

// 輸出
2017-02-26 19:00:27.461 notification[14092:12661907] selector 1
2017-02-26 19:00:27.462 notification[14092:12661907] block 2
2017-02-26 19:00:27.462 notification[14092:12661907] selector 3
2017-02-26 19:00:27.462 notification[14092:12661907] block 4

理解
兩個觀察者,兩個發送者,可見有四個log信息,可以說明,通知支持這種多對多的消息傳遞。

3、疑難點

** 同步or異步**
同步和異步都是相對于發送通知所在的線程的。可以通過下述代碼簡單測出:

// 替換上述的didFinishLaunchingWithOptions函數
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    A *a = [A new];
    [a test];
    self.a = a;
    // 發送方式A:手動定義NSNotification
    NSNotification *noti = [NSNotification notificationWithName:@"111" object:nil];
    [[NSNotificationCenter defaultCenter] postNotification:noti];
    // 發送方式B:自動定義NSNotification
    [[NSNotificationCenter defaultCenter] postNotificationName:@"111" object:nil userInfo:nil];
    NSLog(@"測試同步還是異步");
    return YES;
}

輸出總是為:

2017-02-26 19:13:04.739 notification[15240:12674807] selector 1
2017-02-26 19:13:04.743 notification[15240:12674807] block 2
2017-02-26 19:13:04.743 notification[15240:12674807] selector 3
2017-02-26 19:13:04.744 notification[15240:12674807] block 4
2017-02-26 19:13:04.744 notification[15240:12674807] 測試同步還是異步

可以看出,postNotification:總是會卡住當前線程,待observer執行(如不特殊處理selector也會在postNotification:所在線程執行)結束之后才會繼續往下執行。所以是同步的。

忘記remove的問題
這個就不進行測試了,因為我也沒有iOS8以及之前的設備。直接寫結論了:

  • 若在iOS8或之前版本系統中,對一個對象addObserver:selector:name:object:(假設name為@“111”),但是并沒有在dealloc的之前或之中,對其進行remove操作。那么,在發送通知(name為@“111”)的時候,會因為bad_access(野指針)而crash。
  • 若在iOS9及以后,同上操作,不會crash。

iOS8及以前,NSNotificationCenter持有的是觀察者的unsafe_unretained指針(可能是為了兼容老版本),這樣,在觀察者回收的時候未removeOberser,而后再進行post操作,則會向一段被回收的區域發送消息,所以出現野指針crash。而iOS9以后,unsafe_unretained改成了weak指針,即使dealloc的時候未removeOberser,再進行post操作,則會向nil發送消息,所以沒有任何問題。

三、Notification Queues和異步通知

1、異步通知的原理

創建一個NSNotificationQueue隊列(first in-first out),將定義的NSNotification放入其中,并為其指定三種狀態之一:

typedef NS_ENUM(NSUInteger, NSPostingStyle) {
    NSPostWhenIdle = 1,      // 當runloop處于空閑狀態時post
    NSPostASAP = 2,    // 當當前runloop完成之后立即post
    NSPostNow = 3    // 立即post,同步(為什么需要這種type,且看三.3)
};

這樣,將NSNotification放入queue,然后根據其type,NSNotificationQueue在合適的時機將其post到NSNotificationCenter。這樣就完成了異步的需求。

2、異步通知的使用

// 替換上述的didFinishLaunchingWithOptions函數
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    A *a = [A new];
    [a test];
    self.a = a;
    NSNotification *noti = [NSNotification notificationWithName:@"111" object:nil];
    [[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostASAP];
    //[[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostWhenIdle];
    //[[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostNow];
    NSLog(@"測試同步還是異步");
    return YES;
}

// 輸出
2017-02-26 19:56:32.805 notification[19406:12719309] 測試同步還是異步
2017-02-26 19:56:32.816 notification[19406:12719309] selector 1
2017-02-26 19:56:32.816 notification[19406:12719309] block 2

上述的輸出可以看出,這樣確實完成了異步通知的需求。當然,如果將type改成NSPostNow,則還是同步執行,效果和不用NSNotificationQueue相同。

3、Notification Queues的合成作用

NSNotificationQueue除了有異步通知的能力之外,也能對當前隊列的通知根據NSNotificationCoalescing類型進行合成(即將幾個合成一個)。

typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
    NSNotificationNoCoalescing = 0,  // 不合成
    NSNotificationCoalescingOnName = 1,  // 根據NSNotification的name字段進行合成
    NSNotificationCoalescingOnSender = 2  // 根據NSNotification的object字段進行合成
};

可以通過以下兩種方式進行對比:

// 方式1:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    A *a = [A new];
    [a test];
    self.a = a;
    NSNotification *noti = [NSNotification notificationWithName:@"111" object:nil];
    [[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostASAP coalesceMask:NSNotificationNoCoalescing forModes:nil];
    [[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostWhenIdle coalesceMask:NSNotificationNoCoalescing forModes:nil];
    [[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostNow coalesceMask:NSNotificationNoCoalescing forModes:nil];
    NSLog(@"測試同步還是異步");
    return YES;
}
// 方式2:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    A *a = [A new];
    [a test];
    self.a = a;
    NSNotification *noti = [NSNotification notificationWithName:@"111" object:nil];
    [[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostASAP coalesceMask:NSNotificationCoalescingOnName forModes:nil];
    [[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostWhenIdle coalesceMask:NSNotificationCoalescingOnName forModes:nil];
    [[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostNow coalesceMask:NSNotificationCoalescingOnName forModes:nil];
    NSLog(@"測試同步還是異步");
    return YES;
}
// 輸出
// 方式一
2017-02-26 20:09:31.834 notification[20612:12733161] selector 1
2017-02-26 20:09:31.835 notification[20612:12733161] block 2
2017-02-26 20:09:31.835 notification[20612:12733161] 測試同步還是異步
2017-02-26 20:09:31.851 notification[20612:12733161] selector 3
2017-02-26 20:09:31.851 notification[20612:12733161] block 4
2017-02-26 20:09:31.854 notification[20612:12733161] selector 5
2017-02-26 20:09:31.855 notification[20612:12733161] block 6
// 方式二
2017-02-26 20:11:31.186 notification[20834:12736113] selector 1
2017-02-26 20:11:31.186 notification[20834:12736113] block 2
2017-02-26 20:11:31.186 notification[20834:12736113] 測試同步還是異步

大概完成了合成的需求,但是此處還有一個疑問,比如三個通知合成一個,那么實際發送的NSNotification到底是怎樣的呢?留給讀者們自己挖掘吧。

四、指定Thread處理通知

需求

假設將didFinishLaunchingWithOptions改為:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    A *a = [A new];
    [a test];
    self.a = a;
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    dispatch_async(queue, ^{
        NSLog(@"current thread %@", [NSThread currentThread]);
        [[NSNotificationCenter defaultCenter] postNotificationName:@"111" object:nil];
    });
    return YES;
}

可知,a中的observer的selector將會在DISPATCH_QUEUE_PRIORITY_BACKGROUND中執行,若該selector執行的是刷新UI的操作,那么這種方式顯然是錯誤的。這里,我們需要保證selector永遠在mainThread執行。所以,有以下兩種方式,指定observer的回調方法的執行線程。

解決方式1:NSMachPort

// 代碼
#import <CoreFoundation/CFRunLoop.h>
@interface A : NSObject <NSMachPortDelegate>
@property NSMutableArray *notifications;
@property NSThread *notificationThread;
@property NSLock *notificationLock;
@property NSMachPort *notificationPort;
- (void)setUpThreadingSupport;
- (void)handleMachMessage:(void *)msg;
- (void)processNotification:(NSNotification *)notification;
- (void)test;
@end
@implementation A
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)test {
    [self setUpThreadingSupport];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:@"111" object:nil];
}
- (void)setUpThreadingSupport {
    if (self.notifications) {
        return;
    }
    self.notifications      = [[NSMutableArray alloc] init];
    self.notificationLock   = [[NSLock alloc] init];
    self.notificationThread = [NSThread mainThread];
    self.notificationPort = [[NSMachPort alloc] init];
    [self.notificationPort setDelegate:self];
    [[NSRunLoop currentRunLoop] addPort:self.notificationPort
                                forMode:(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] != self.notificationThread) {
        [self.notificationLock lock];
        [self.notifications addObject:notification];
        [self.notificationLock unlock];
        [self.notificationPort sendBeforeDate:[NSDate date]
                                   components:nil
                                         from:nil
                                     reserved:0];
    } else {
        NSLog(@"current thread %@ 刷新UI", [NSThread currentThread]);
        // 刷新UI ...
    }
}
@end

// 輸出
2017-02-27 11:49:04.296 notification[29036:12827315] current thread <NSThread: 0x7be3e000>{number = 3, name = (null)}
2017-02-27 11:49:04.307 notification[29036:12827268] current thread <NSThread: 0x7bf3b970>{number = 1, name = main} 刷新UI

由輸出可知,雖然post不是在主線程,但是刷新UI確實是在主線程,達成需求。這是官方文檔提供的一種方式,就不具體解釋了,也很容易看懂。不懂去看文檔吧。

解決方式2:block方式addObserver

文檔提供的方式一難用且冗長,這里我們可以用下述方式替代。

 // 代碼
@interface A : NSObject <NSMachPortDelegate>
- (void)test;
@end
@implementation A
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)test {
    [[NSNotificationCenter defaultCenter] addObserverForName:@"111" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        NSLog(@"current thread %@ 刷新UI", [NSThread currentThread]);
        // 刷新UI ...
    }];
}
@end
 
// 輸出
current thread <NSThread: 0x7bf29110>{number = 3, name = (null)}
2017-02-27 11:53:46.531 notification[29510:12833116] current thread <NSThread: 0x7be1d6f0>{number = 1, name = main} 刷新UI

完美!方便!

五、通知實現解析

這里我參見的是偽源碼
源碼很多,不一一解釋,我關注的只有兩個點:

  • observer的對象存儲在何處?
  • post的時候根據何種方式查找接受通知的對象?

observer的對象存儲在何處

// NSNotificationCenter的init方法
- (id) init
{
  if ((self = [super init]) != nil)
    {
      _table = newNCTable();
    }
  return self;
}

這也就很容易看出,每個NSNotificationCenter都有一個默認的_table。其對observer進行引用(iOS9以前unsafe_unretained,iOS9以后weak)。

post的時候根據何種方式查找接受通知的對象

// postNotification的部分代碼
/*
 * Find the observers that specified OBJECT, but didn't specify NAME.
 */
if (object) {
  n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
  if (n != 0)  {
    o = purgeCollectedFromMapNode(NAMELESS, n);
    while (o != ENDOBS)  {
      GSIArrayAddItem(a, (GSIArrayItem)o);
      o = o->next;
    }
  }
}

/*
 * Find the observers of NAME, except those observers with a non-nil OBJECT
 * that doesn't match the notification's OBJECT).
 */
if (name) {
  n = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name));
  if (n) {
    m = (GSIMapTable)n->value.ptr;
  } else {
     m = 0;
  }
  if (m != 0) {
  /*
   * First, observers with a matching object.
   */
    n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
    if (n != 0) {
      o = purgeCollectedFromMapNode(m, n);
      while (o != ENDOBS) {
        GSIArrayAddItem(a, (GSIArrayItem)o);
        o = o->next;
      }
    }
    if (object != nil) {
    /*
     * Now observers with a nil object.
     */
        n = GSIMapNodeForSimpleKey(m, (GSIMapKey)nil);
        if (n != 0) {
          o = purgeCollectedFromMapNode(m, n);
          while (o != ENDOBS) {
            GSIArrayAddItem(a, (GSIArrayItem)o);
            o = o->next;
        }
      }
    }
  }
}

不用完全看懂這塊代碼,只需要看出,在table中查找observer object的時候,首先根據的是object,接下來根據的是name,可見name的優先級比較高。

六、拾遺

1、那些系統的通知Name

// 當程序被推送到后臺時
UIKIT_EXTERN NSNotificationName const UIApplicationDidEnterBackgroundNotification       NS_AVAILABLE_IOS(4_0);
// 當程序從后臺將要重新回到前臺時
UIKIT_EXTERN NSNotificationName const UIApplicationWillEnterForegroundNotification      NS_AVAILABLE_IOS(4_0);
// 當程序完成載入后通知
UIKIT_EXTERN NSNotificationName const UIApplicationDidFinishLaunchingNotification;
// 應用程序轉為激活狀態時
UIKIT_EXTERN NSNotificationName const UIApplicationDidBecomeActiveNotification;
// 用戶按下主屏幕按鈕調用通知,并未進入后臺狀態
UIKIT_EXTERN NSNotificationName const UIApplicationWillResignActiveNotification;
// 內存較低時通知
UIKIT_EXTERN NSNotificationName const UIApplicationDidReceiveMemoryWarningNotification;
// 當程序將要退出時通知
UIKIT_EXTERN NSNotificationName const UIApplicationWillTerminateNotification;
// 當系統時間發生改變時通知
UIKIT_EXTERN NSNotificationName const UIApplicationSignificantTimeChangeNotification;
// 當StatusBar框方向將要變化時通知
UIKIT_EXTERN NSNotificationName const UIApplicationWillChangeStatusBarOrientationNotification __TVOS_PROHIBITED; // userInfo contains NSNumber with new orientation
// 當StatusBar框方向改變時通知
UIKIT_EXTERN NSNotificationName const UIApplicationDidChangeStatusBarOrientationNotification __TVOS_PROHIBITED;  // userInfo contains NSNumber with old orientation
// 當StatusBar框Frame將要改變時通知
UIKIT_EXTERN NSNotificationName const UIApplicationWillChangeStatusBarFrameNotification __TVOS_PROHIBITED;       // userInfo contains NSValue with new frame
// 當StatusBar框Frame改變時通知
UIKIT_EXTERN NSNotificationName const UIApplicationDidChangeStatusBarFrameNotification __TVOS_PROHIBITED;        // userInfo contains NSValue with old frame
// 后臺下載狀態發生改變時通知(iOS7.0以后可用)
UIKIT_EXTERN NSNotificationName const UIApplicationBackgroundRefreshStatusDidChangeNotification NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED;
// 受保護的文件當前變為不可用時通知
UIKIT_EXTERN NSNotificationName const UIApplicationProtectedDataWillBecomeUnavailable    NS_AVAILABLE_IOS(4_0);
// 受保護的文件當前變為可用時通知
UIKIT_EXTERN NSNotificationName const UIApplicationProtectedDataDidBecomeAvailable       NS_AVAILABLE_IOS(4_0);
// 截屏通知(iOS7.0以后可用)
UIKIT_EXTERN NSNotificationName const UIApplicationUserDidTakeScreenshotNotification NS_AVAILABLE_IOS(7_0);

2、通知的效率

該用就用,不用特別在意。

七、文獻

1、https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Notifications/Introduction/introNotifications.html#//apple_ref/doc/uid/10000043-SW1
2、https://github.com/gnustep/base/tree/master/Source

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容