NSNotification 你不知道的使用技巧

本文详细介绍了NSNotification的使用,包括基本操作如发送、接收和移除观察者,以及注意事项和性能优化。讨论了同步执行、指定处理队列、NSNotification对象传递的问题以及如何利用Runtime拦截通知。还探讨了NSNotificationQueue的使用,如通知聚合和与RunLoopMode的关联。提供了一些关键的实践建议和参考资料。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

NSNotification使用姿势

基本使用

发送通知:

- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
  • aName:通知名称
  • anObject:发送者(sender)
  • aUserInfo:关于这个通知的相关信息

接收通知(注册观察者):

- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
  • observer:观察者
  • aSelector:观察者的回调方法
  • aName:指定通知名称,为nil则接收任意通知
  • anObject:指定发送者(sender),为nil则可以接收任意发送者的通知

  • 观察者只能收到注册以后sender发出的通知。
  • 如果app内存在多个同名的Notification,观察者可以通过object参数指定发送者。
  • 观察者可以多次addObserver同一个通知,这样会导致selector被多次调用。
  • 同一个NSNotification可以被多次post

移除观察者:

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

iOS8及以前,NSNotificationCenter持有的是观察者的unsafe_unretained指针(可能是为了兼容老版本),这样,在观察者回收的时候未removeOberser,而后再进行post操作,则会向一段被回收的区域发送消息,所以出现野指针crash。而iOS9以后,unsafe_unretained改成了weak指针,即使dealloc的时候未removeOberser,再进行post操作,则会向nil发送消息,所以没有任何问题。

注意事项

  • 每当发送一个广播,NSNotificationCenter会遍历所有观察者,对于频繁发送的通知,应该创建一个专门的NSNotificationCenter来添加观察者和post通知,这样可以减少遍历带来的性能损耗。
  • NSNotificationCenter/NSNotification是线程安全的

If your app uses notifications extensively, you may want to create and post to your own notification centers rather than posting only to the defaultCenternotification center. When a notification is posted to a notification center, the notification center scans through the list of registered observers, which may slow down your app. By organizing notifications functionally around one or more notification centers, less work is done each time a notification is posted, which can improve performance throughout your app.

同步执行

观察者的回调方法是在发送方的线程同步执行的:

  • 当pose一个通知之后,NSNotificationCenter就会遍历dispatch_table去执行对应观察者的回调方法。
  • 回调方法的执行线程与post通知的线程一致。如果接收到通知后需要更新UI的话,需要确保post的线程是主线程,或者接收到通知后dispatch到主线程执行

指定处理队列

- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name 
                            object:(nullable id)obj 
                             queue:(nullable NSOperationQueue *)queue 
                        usingBlock:(void (^)(NSNotification *note))block API_AVAILABLE(mios(4.0));

指定主线程接收通知:

使用block方式注册观察者要注意:

  • block导致的循环引用
  • 移除观察者,把返回值保存起来,利用返回值来解除注册。

NSNotification 对象传递导致的问题

因为一个NSNotification可能会有多个Observer,而这些Observer的回调方法是串行执行的,所以在这些回调方法传递过程中会不会出现一些问题呢?

  • 发送方和接收方处理的是同一个对象。
  • NSNotificationCenter会按照addObserver的顺序执行观察者的回调方法。

因为发送方和接收方处理的是同一个对象,那么能不能在第一次接收到notification的时候对sender信息进行篡改呢?

因为NSNotification里面的属性都是readonly,尝试采用KVC的方式:

  • 在第一次接收到通知的时候,更改了notification的sender object为nil,但是仍然可以被第二次接收到,证明了NSNotification无法被前面注册的观察者拦截。

如果前面的观察者修改了userInfo信息,后面观察者接收到的userInfo会是?

  • 上面代码证明了userInfo是可以被前面的观察者修改的,这个修改会传递到后面的观察者。

使用Runtime的方式来拦截通知

使用object_setClass可以在运行时改变一个对象的isa指针,使这个对象变为另一个类。所以我们可以自定义一个TTNotificationCenter,继承于原来的NSNotification,重写发送通知的几个方法。利用object_setClass将defaultCenter的类型设为TTNotificationCenter。

实现代码:

@interface TTNotificationCenter : NSNotificationCenter
@end

@implementation TTNotificationCenter
- (void)postNotificationName:(NSNotificationName)aName object:(id)anObject userInfo:(NSDictionary *)aUserInfo {
    NSLog(@"[hack1]postNotification: %@", aName, anObject, aUserInfo);
}

- (void)postNotificationName:(NSNotificationName)aName object:(id)anObject {
    NSLog(@"[hack2]postNotification: %@", aName);
}

- (void)postNotification:(NSNotification *)notification {
    NSLog(@"[hack3]postNotification: %@", notification.name);
}
@end

void hackNSNotification() {
    id center = [NSNotificationCenter defaultCenter];
    object_setClass(center, [TTNotificationCenter class]);
}

测试代码:

运行结果:

在调试中可以看出系统通知的一些特征:

  • 采用postNotificationName发布通知。
  • 会把自己设置为发送方

通过NSNotificationQueue发布通知

使用方式

  • 通过[NSNotificationQueue defaultQueue]获取全局的默认队列,使用enqueue…方法将通知提交到队列中,由队列在指定的时机发送通知。
  • 使用NSNotificationQueue还是会在当前线程发布通知和通知处理。
NSPostingStyle
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
    NSPostWhenIdle = 1,
    NSPostASAP = 2,
    NSPostNow = 3
};

NSNotificationQueue中的NSNotification的提交时机,

  • NSPostWhenIdle 在runloop进入waiting状态时发送,当runloop要退出时,不会发送
  • NSPostASAP Posting As Soon As Possible,在runloop的当前迭代完成时发送给通知中心
  • NSPostNow 同步提交,相当于postNotification

测试NSPostWhenIdle:

  • 特征:需要runloop进入waiting状态才post通知

测试NSPostASAP:

  • 特征,先执行完当前runloop中的notification、timer。

NSNotificationCoalescing

typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
    NSNotificationNoCoalescing = 0,
    NSNotificationCoalescingOnName = 1,
    NSNotificationCoalescingOnSender = 2
};
  • 通知聚合,在同一个队列中的根据聚合条件将多个通知聚合为一个通知。默认是:NSNotificationCoalescingOnName | NSNotificationCoalescingOnSender。

以下代码设置了NSNotificationCoalescing:NSNotificationCoalescingOnSender,这将聚合同一个发送者的通知:

后面enqueue的通知被聚合到第一个通知TTNotification1。

与RunLoopMode关联

在主线程中执行以下代码,观察者将无法得到通知:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"发布通知线程:%@, runloop:%@", [NSThread currentThread], [NSRunLoop currentRunLoop]);
        [[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostWhenIdle];
    });
  • (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
    这个方法默认指定runloop mode为NSDefaultRunLoopMode,当子线程未开启RunLoop时,自然无法发出通知。

让Runloop跑起来:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //发送通知
        NSLog(@"发布通知线程:%@", [NSThread currentThread]);
        [[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostWhenIdle];

        //启动runloop
        [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] run];
    });

参考资料

  1. NSNotificationCenter 进阶及自定义https://juejin.im/post/5b97825cf265da0ac138fca5
  2. 深入理解NSNotification https://philm.gitbook.io/philm-ios-wiki/mei-zhou-yue-du/shen-ru-si-kao-nsnotification
  3. RunLoop简介 https://www.jianshu.com/p/94d61de9e139
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值