如人饮水,冷暖自知

iOS 模式详解—「观察者模式:NSNotificationCenter 通知中心」

引导


Notification(通知) 是 iOS 系统下重要的消息传递机制之一,通知封装了诸如窗口获得焦点、网络连接关闭等事件信息,通知的内容可按照我们实际的需求来定制。在实际开发中或多或少都会接触到,NSNotificationCenter 与 其它对象之间通信方式类似,但也存在不同,我们需要根据具体应用场景选择(或优先选择)恰当的通信方式。

本篇文章主要从【NSNotification & NSNotificationCenter 使用&注意点】学习总结,该模块学习将续更 ~
在「时间 & 知识 」有限内,总结的文章难免有「未全、不足 」的地方,还望各位好友指出,可留言指正或是补充,以提高文章质量@白开水ln原著;

目录:

  1. NSNotification
    1.NSNotification 概念
    2.NSNotification.h 系统文件
  2. NSNotificationCenter
    1.NSNotificationCenter 概念
    2.NSNotificationCenter 系统文件
  3. NSNotificationCenter 的使用流程
    1.注册观察者(添加监听)
    2.发送通知
    3.移除观察者(移除监听)
  4. NSNotificationQueue
    1.NSNotificationQueue 概念
    2.NSNotificationQueue.h 系统文件
    3.NSNotificatinonCenter 实现原理
  5. NSNotificatinonCenter 实战使用
    1.基本使用
    2.通知在多线程中使用
    3.通知在多线程中注意点
  6. NSNotification.h
  7. SourceCodeToolsClassWechatPublic-Codeidea

NSNotification


本着好好学习,了解权威的目的,我们还是主动看官网的说明。

 NSNotification iOS 2.0+

NSNotification 概念

上图简单点释义就是
NSNotification是方便NSNotificationCenter 广播到其他对象时的封装对象,简单讲即通知中心对通知调度表中的对象广播时发送NSNotification对象。
NSNotification对象(称为通知)包含名称、object 和一个可选字典三个属性,名称是用来标识通知的标记(一般为常量字符串),object是任意想要携带的对象(通常为发送者自己 或为nil),字典用来存储发送通知时附带的信息(可为nil)。NSNotification 对象是不可变的对象。

NSNotification.h 系统文件

看完官网,接下来当然是看系统文件了。你说是吧!

1
2
3
4
5
6
7
8
9
10
/**************** Notifications ****************/
@interface NSNotification : NSObject <NSCopying, NSCoding>
@property (readonly, copy) NSNotificationName name;// 通知的标识名称(一般为常量字符串)
@property (nullable, readonly, retain) id object;// 任意想要携带的对象(通常为发送者自己,可为nil),
@property (nullable, readonly, copy) NSDictionary *userInfo;// 关于通知的附加信息(可为nil)
- (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;

接下来看下三个初始化方法

1
2
3
4
5
+ (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 */

NSNotificationCenter


我们还是主动看官网的说明。

 NSNotificationCenter iOS 8.0+

NSNotificationCenter 概念

上图简单点释义就是
NSNotificationCenter对象(通知中心) 是 Foundation 框架的一个子系统,提供了在程序中广播消息的机制。通过[NSNotificationCenter defaultCenter]获取引用总的通知中心,可以在不同类之间通信的时候使用。

在通知中心注册观察者,发送者使用通知中心广播时,以NSNotificationnameobject来确定需要发送给哪个观察者。为保证观察者能接收到通知,所以应先向通知中心注册观察者,接着再发送通知这样才能在通知中心调度表中查找到相应观察者进行通知。

 NSNotificationCenter

NSNotificationCenter 系统文件

看完官网,接下来当然是看系统文件了。你说是吧!

1
2
3
4
5
6
7
8
9
10
11
12
/**************** Notification Center ****************/
@interface NSNotificationCenter : NSObject {
@package
void *_impl;
void *_callback;
void *_pad[11];
}
#if FOUNDATION_SWIFT_SDK_EPOCH_AT_LEAST(8)
// 获取通知中心
@property (class, readonly, strong) NSNotificationCenter *defaultCenter;

总结:获取NSNotificationCenter的方法只有一种,即[NSNotificationCenter defaultCenter],并且NSNotificationCenter是一个单例模式,一旦创建,这个通知中心的对象会一直存在于一个应用的生命周期。

通知中心的使用流程

获取通知中心对象后,我们就可以使用它来处理通知相关的操作了,包括注册观察者、发送通知 和 移除观察者。

1、注册观察者(添加监听)

你可以使用以下两种方式注册观察者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//--------------------------- 注册观察者方式一 ------------------------------//
/**
Observer: 观察者
selector: 只要一监听到通知,就会调用观察者这个方法
Name: 通知名称也是通知的唯一标示,编译器就是通过这个找到通知的。
object: 谁发出的通知,表示会对哪个发送者对象发出的事件作出响应,nil 时表示接受所有发送者的事件。
*/
- (void)addObserver:(id)observer selector:(SEL)aSelector
name:(nullable NSNotificationName)aName
object:(nullable id)anObject;
//--------------------------- 注册观察者方式二 ------------------------------//
/**
Name: 通知名称
object: 谁发出的通知
queue: 决定block在哪个线程执行,nil:在发布通知的线程中执行
[NSOperationQueue mainQueue]: 一般都是使用主队列
usingBlock: 只要监听到通知,就会执行这个block
注意: 一定要记得移除
*/
- (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);
// The return value is retained by the system, and should be held onto by the caller in
// order to remove the observer with removeObserver: later, to stop observation.

总结

  • 第一种方式是比较常用的添加Oberver的方式,接到通知时执行Selector方法,观察者接收到通知后执行任务的代码在发送通知的线程中执行(下面示例代码验证)。

  • 第二种方式是提供了一个以block方式实现的添加观察者的方法。大家第一次看到这个方法时是否会有这样的疑问:观察者呢?参数中并没有指定具体的观察者,那谁是观察者呢?需要移除吗?

  • 实际上,与前一个方法不同的是,前者使用一个现存的对象作为观察者,而这个方法会创建一个匿名的对象作为观察者(即方法返回的id<NSObject>对象),这个匿名对象会在指定的队列(queue)上去执行我们的block

第二种注册方式注意点

  • 1、参数queue 决定block在哪个线程执行,即我们指定了操作队列。如果queuenil,则消息是默认在post线程中同步处理,即 观察者接收到通知后执行任务的代码在发送通知的线程中执行(下面示例代码验证)。

  • 2、block块会被通知中心拷贝一份(执行copy操作),以在堆中维护一个block对象,直到观察者被从通知中心中移除。所以,应该特别注意在block中使用外部对象,避免出现对象的循环引用

  • 3、如果一个给定的通知触发了多个观察者的block操作,则这些操作会在各自的Operation Queue中被并发执行。所以我们 不能去假设操作的执行会按照添加观察者的顺序来执行

  • 4、该方法会返回一个表示观察者的对象,记得在不用时移除这个对象

第2点示例,由于使用的是block,所以需要注意的就是避免引起循环引用的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@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:一定要注意这个问题。

2、发送通知

发送通知可使用以下方法

1
2
3
- (void)postNotification:(NSNotification *)notification;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;

总结

  • 三种方式都是发送NSNotification对象给通知中心注册的所有观察者。

  • 发送通知通过nameobject来确定来标识观察者,nameobject两个参数的规则相同即当通知设置namekChangeNotifition时,那么只会发送给符合namekChangeNotifition的观察者,同理object指发送给某个特定对象通知,如果只设置了name,那么只有对应名称的通知会触发。如果同时设置nameobject参数时就必须同时符合这两个条件的观察者才能接收到通知。

3、移除观察者(移除监听)

在对象被释放前需要移除掉观察者,避免已经被释放的对象还接收到通知导致崩溃。
移除观察者有两种方式:

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

总结

  • 传入相应的需要移除的observer 或者使用第二种方式三个参数来移除指定某个观察者。

  • 如果使用基于-[NSNotificationCenter addObserverForName:object:queue:usingBlock:]方法在获取方法返回的观察者进行释放。基于这个方法我们还可以让观察者接到通知后只执行一次:

    1
    2
    3
    4
    __block __weak id<NSObject> observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"note" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
    NSLog(@"%@",[NSThread currentThread]);
    }];
  • 通知的发送与处理是同步的,在某个地方post一个消息时,会等到所有观察者对象执行完处理操作后,才回到post的地方,继续执行后面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reciveNote1) name:@"note" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reciveNote2) name:@"note" object:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:@"note" object:nil];
NSLog(@"continue");
}
- (void)reciveNote1 {
NSLog(@"接收到通知--reciveNote1");
}
- (void)reciveNote2 {
NSLog(@"接收到通知--reciveNote2");
}
//--------------------------- <#我是分割线#> ------------------------------//
//
打印输出
2016-03-28 15:33:48.423 03-通知补充[8711:298506] 接收到通知--reciveNote1
2016-03-28 15:33:48.423 03-通知补充[8711:298506] 接收到通知--reciveNote2
2016-03-28 15:33:48.423 03-通知补充[8711:298506] continue

NSNotificationQueue


我们还是主动看官网的说明。

 NSNotificationQueue iOS 2.0+

NSNotificationQueue 概念

上图简单点释义就是
NSNotificationQueue 通知队列,更像是通知中心的缓冲区,用来管理多个通知的调用。通知队列通常以先进先出(FIFO)顺序管理通知。当一个通知上升到队列的前面时,队列就将它发送给通知中心(NSNotificationCenter),通知中心随后将它派发给所有注册为观察者的对象。。

 NSNotificationQueue

NSNotificationQueue.h 系统文件

看完官网,接下来当然是看系统文件了。你说是吧!

  • 创建通知队列方法
1
2
// 创建通知队列方法
- (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter NS_DESIGNATED_INITIALIZER;
  • 往队列加入通知方法(异步)
1
2
3
// 往队列加入通知方法
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSNotificationCoalescing)coalesceMask forModes:(nullable NSArray<NSRunLoopMode> *)modes;
  • 移除队列中的通知方法
1
2
// 移除队列中的通知方法
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;
  • 发送方式
1
2
3
4
5
6
7
8
9
10
// NSPostingStyle包括三种类型
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
NSPostWhenIdle = 1,
NSPostASAP = 2,
NSPostNow = 3
};
注解:
NSPostWhenIdle:空闲发送通知 当运行循环处于等待或空闲状态时,发送通知,对于不重要的通知可以使用。
NSPostASAP:尽快发送通知 当前运行循环迭代完成时,通知将会被发送,有点类似没有延迟的定时器。
NSPostNow :同步发送通知 如果不使用合并通知 和postNotification:一样是同步通知。
  • 合并通知
    • 通过合并我们可以用来保证相同的通知只被发送一次。
    • forModes:(nullable NSArray<NSRunLoopMode> *)modes可以使用不同的NSRunLoopMode配合来发送通知,可以看出实际上NSNotificationQueueRunLoop的机制以及运行循环有关系,通过NSNotificationQueue队列来发送的通知和关联的RunLoop运行机制来进行的。
1
2
3
4
5
6
7
8
9
10
// NSNotificationCoalescing也包括三种类型
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
NSNotificationNoCoalescing = 0,
NSNotificationCoalescingOnName = 1,
NSNotificationCoalescingOnSender = 2
};
注解:
NSNotificationNoCoalescing:不合并通知。
NSNotificationCoalescingOnName:合并相同名称的通知。
NSNotificationCoalescingOnSender:合并相同通知和同一对象的通知。

NSNotificatinonCenter 实现原理


  • NSNotificatinonCenter是使用观察者模式来实现的用于跨层传递消息,用来降低耦合度。

  • NSNotificatinonCenter用来管理通知,将观察者注册到NSNotificatinonCenter的通知调度表中,然后发送通知时利用标识符nameobject识别出调度表中的观察者,然后调用相应的观察者的方法,即传递消息(在Objective-C中对象调用方法,就是传递消息,消息有name或者selector,可以接受参数,而且可能有返回值),如果是基于block创建的通知就调用NSNotificationblock

NSNotificatinonCenter 实战使用


基本使用

第一种注册观察者的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)viewDidLoad {
[super viewDidLoad];
// 1.注册观察者(添加监听)
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reciveNote1) name:@"note" object:nil];
// 2.发送通知
[[NSNotificationCenter defaultCenter] postNotificationName:@"note" object:nil];
}
// 一个对象即将销毁就会调用
- (void)dealloc {
// 3.移除通知
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
// 观察者接收到通知后执行任务的代码
- (void)reciveNote1 {
NSLog(@"接收到通知--reciveNote1");
}
打印输出:
2016-03-28 16:57:45.320 03-通知补充[10343:376649] 接收到通知--reciveNote1

第二种注册观察者的方式:

1
2
3
4
5
6
7
8
9
@property (nonatomic, weak) id observe;
// 1.注册观察者(添加监听)
_observe = [[NSNotificationCenter defaultCenter] addObserverForName:@"note" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
NSLog(@"我是block方式注册观察者");
}];
// 2.发送通知
[[NSNotificationCenter defaultCenter] postNotificationName:@"note" object:nil];

总结:这个方法的优点在于添加观察者的操作与回调处理操作的代码更加紧凑,不需要拼命滚动鼠标就能直接找到处理代码,简单直观。个人比较喜欢。

通知在多线程中使用

注册观察者两种方式,在多线程中的使用和注意点,以下 会分别说明。

第一种注册观察者方式,以下代码我们将验证这个结论:
接收通知代码 由 发出通知线程决定,即观察者接收到通知后执行任务的代码在发送通知的线程中执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1.注册观察者(添加监听)
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reciveNote1) name:@"note" object:nil];
// 2.发送通知(主线程)
[[NSNotificationCenter defaultCenter] postNotificationName:@"note" object:nil];
// 发送通知(异步线程)
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"note" object:nil];
});
//--------------------------- <#我是分割线#> ------------------------------//
//
// 接收到通知执行代码
- (void)reciveNote1 {
NSLog(@"%@",[NSThread currentThread]);
NSLog(@"接收到通知--reciveNote1");
}

1
2
3
4
5
6
7
发送通知(异步线程)打印输出:
2016-03-28 17:21:36.122 03-通知补充[10818:402396] <NSThread: 0x600000267cc0>{number = 3, name = (null)}
2016-03-28 17:21:36.122 03-通知补充[10818:402396] 接收到通知--reciveNote1
发送通知(主线程)打印输出:
2016-03-28 17:23:42.705 03-通知补充[10868:404679] <NSThread: 0x60800007a6c0>{number = 1, name = main}
2016-03-28 17:23:42.705 03-通知补充[10868:404679] 接收到通知--reciveNote1

第二种注册观察者方式,以下代码我们将验证这个结论:
接收通知代码 由 发出通知线程决定,即观察者接收到通知后执行任务的代码在发送通知的线程中执行(指定操作队列除外)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 指定操作队列 [NSOperationQueue mainQueue]
// _observe = [[NSNotificationCenter defaultCenter] addObserverForName:@"note" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
// 1.注册观察者(添加监听)
_observe = [[NSNotificationCenter defaultCenter] addObserverForName:@"note" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
// 只要监听到通知 就会调用
NSLog(@"%@",[NSThread currentThread]);
NSLog(@"我是block方式注册观察者");
}];
// 2.发送通知(主线程)
[[NSNotificationCenter defaultCenter] postNotificationName:@"note" object:nil];
// 发送通知(异步线程)
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"note" object:nil];
});

1
2
3
4
5
6
7
8
9
10
11
发送通知(主线程)打印输出:
2016-03-28 18:17:08.587 03-通知补充[11305:421194] <NSThread: 0x608000072940>{number = 1, name = main}
2016-03-28 18:17:08.587 03-通知补充[11305:421194] 我是block方式注册观察者
发送通知(异步线程)打印输出:
2016-03-28 18:24:56.248 03-通知补充[11503:431482] <NSThread: 0x60000007ec40>{number = 3, name = (null)}
2016-03-28 18:24:56.248 03-通知补充[11503:431482] 我是block方式注册观察者
注册观察者,指定操作队列(不管发送通知在那个线程都按指定操作队列执行)
2016-03-28 18:27:04.567 03-通知补充[11555:433722] <NSThread: 0x600000073e40>{number = 1, name = main}
2016-03-28 18:27:04.567 03-通知补充[11555:433722] 我是block方式注册观察者
通知在多线程中注意点

开发中使用场景,第一种方式,一般在接收通知执行代码中,做一下处理:

1
2
3
4
5
6
7
8
9
- (void)reciveNote1
{
// 更新UI
dispatch_sync(dispatch_get_main_queue(), ^{
// 更新UI
NSLog(@"%@",[NSThread currentThread]);
});
}

第二种方式,一般在接收通知执行代码中,做一下处理:

1
2
3
4
5
6
7
// 一般不要写nil ,更新UI指定主队列
_observe = [[NSNotificationCenter defaultCenter] addObserverForName:@"note" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
// 更新UI
NSLog(@"%@",[NSThread currentThread]);
}];

总结


在我们的应用程序中,两个对象之间如何通信。根据具体应用场景优先选择哪一种通信方式。
对象之间的通信方式主要有以下几种:

  • 直接方法调用
  • Target-Action事件
  • Delegate代理
  • block回调
  • KVO监听
  • NSNotification通知

优先选择哪一种通信方式:

通信对象是一对一的还是一对多的
对象之间的耦合度.

建议:

  • 1.在需要的地方使用通知,要求: 必须得保证通知的名称在监听和发出时是一致的。
  • 2.注册的观察者在不使用时一定要记得移除,即添加和移除要配对出现。
  • 3.尽可能迟地去注册一个观察者,并尽可能早将其移除,这样可以改善程序的性能。因为,每post一个通知,都会是遍历通知中心的分发表,确保通知发给每一个观察者。
  • 4.记住通知的发送和处理是在同一个线程中。
  • 5.使用-addObserverForName:object:queue:usingBlock:务必处理好内存问题,避免出现循环引用。
  • 6.NSNotificationCenter是线程安全的,但并不意味着在多线程环境中不需要关注线程安全问题。不恰当的使用仍然会引发线程问题。

Reading


  • 如果在阅读过程中遇到 error || new ideas,希望你能 issue 我,我会及时补充谢谢。

  • 不管谁的博客上面写的 (也包括自己),阅读的你要敢于去验证;如人饮水,冷暖自知;(共勉)。

  • 喜欢可 赞赏 or Star一波;点击左上角关注 或 微众『Codeidea』,在 Demo 更新时收到邮件通知,便捷你的阅读。

🖋 Plain boiled water ln : 本文结束    感谢阅读 ^_^. Need coffee?
👁 At this time suggest 1 minute eye exercises

本文标题:iOS 模式详解—「观察者模式:NSNotificationCenter 通知中心」

文章作者:白开水ln

原始链接:http://plainboiledwaterln.cn/iOSUI/NSNotification.html

版权声明: 署名-非商业性使用-禁止演绎 4.0 国际 『微众圈:Codeidea』本博客文章除特别声明外均为原创,如需转载请务必保留原链接(可点击进入的链接)和作者出处,谢谢合作!

「喜欢就留言or赞赏」but「支持不要超过你早餐费的 0.5」 ^_^.