你可能不知道的Notification

Notification,项目中使用还是蛮多的,post发送通知,addObserver监听接收通知,听起来很简单,对吧。但是还是会有些大家可能会忽视的地方。

同步or异步

[NotificationCenter defaultCenter] postNotification],这种方式是同步的,并且在哪个线程发,就在哪个线程收。

同步的意思,就是消息接收者全部处理完消息之后,post这方才会继续往下执行,因此,尽量不要做太耗时的操作。

由于消息收和发都在同一个线程中。所以,尽量在主线程中post,不然会引起不必要的麻烦,ui刷新问题,崩溃问题等等。

addObserver调用多次

addObserver如果添加多次,当post的时候,也会收到多次。类似这种:

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

observer的移除

这个是老生常谈了,一定记得要移除,否则崩溃很容易发生。不过在iOS 9以后,不需要手动移除。

If your app targets iOS 9.0 and later or OS X v10.11 and later, you don't need to unregister an observer in its deallocation method。

异步通知

异步的好处,不必等待所有的消息处理者处理完成,可以立马返回。

如果要发送异步通知,可以使用NSNotificationQueue,将通知enqueueNotification之后,会在合适的时候将通知发送给NotificationCenter,NotificationCenter会真正的将消息post出去。

[[NSNotificationQueue defaultQueue] enqueueNotification:[NSNotification notificationWithName:@"task" object:self] postingStyle:NSPostWhenIdle];

可以指定runloop mode,当runloop处理该种mode的时候,才会发送通知。

也可以指定发送通知的时机,有如下3种。

typedef NS_ENUM(NSUInteger, NSPostingStyle) {
    NSPostWhenIdle = 1,
    NSPostASAP = 2,
    NSPostNow = 3
};

NSPostWhenIdle,在runloop空闲时发送,当runloop要退出时,不会发送。

NSPostASAP:Posting As Soon As Possible,在runloop的当前迭代完成时发送给通知中心,但是当前mode和设定的mode要一致。

NSPostNow:就是同步调用。

聚合发送

当在一段时间内,enqueue了多个通知,系统不会每个都发送,如果在队列中已有该种通知,则不会进入队列,只保留第一个通知。有如下3中可选方式。

typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
    NSNotificationNoCoalescing = 0,         // 不聚合
    NSNotificationCoalescingOnName = 1,         // 根据通知名聚合
    NSNotificationCoalescingOnSender = 2       // 根据发送者聚合
};

NSNotificationCoalescingOnName:根据通知名称来聚合,如果在一段时间内,
NotificationName="UpdateMyProfileNotification"有多个,则将他们聚合起来,只发送一个。

NSNotificationCoalescingOnSender:根据发送方来聚合。

我做了下测试,的确只收到一次通知。并且是第一个通知。

for (int i = 0; i < 2; i++) {
        NSNotification *notification = [NSNotification notificationWithName:@"TestNotification" object:@(i)];
        [[NSNotificationQueue defaultQueue] enqueueNotification:notification                     
        postingStyle:NSPostASAP coalesceMask:NSNotificationCoalescingOnName forModes:nil];
    }

在指定线程接收通知

上面说到,收发都在一个线程中,如果想要做到,在某个指定的线程接收通知,该如何做呢?苹果文档上提及了实现思路。

先简单介绍下mach port,它主要用来线程间通信。简单来说,就是接收线程中注册NSMachPort,在另外的线程中使用此port发送消息,则注册线程会收到相应消息,调用handleMachMessage来处理。

主要思路:

  1. 定义一个中间对象NotificationHandler,用来专门接收通知,包括一个队列,接收通知的线程,mach port,lock。

  2. 首先NotificationHandler会注册一个通知,对应的处理函数为processNotification,当在其他线程中post时,processNotification会被调用,进行如下处理。如果收到的通知跟指定的线程一样,则处理消息,反之,则添加到队列,同时通过port发送消息给指定线程。注意多线程中,对队列的处理,要加锁

  3. 指定线程收到回调handleMachMessage,首先会将通知删除,然后调用processNotification进行处理,继续以上过程。


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

        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:@"notification" object:nil];
    }

    return self;
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)setUpThreadingSupport
{
    if (self.notifications) {
        return;
    }
    self.notifications = [NSMutableArray new];
    self.lock = [NSLock new];
    self.thread = [NSThread currentThread];

    self.port = [NSMachPort new];
    [self.port setDelegate:self];
    
    [[NSRunLoop currentRunLoop] addPort:self.port forMode:(__bridge NSString *) kCFRunLoopCommonModes];
}

- (void)handleMachMessage:(void *)msg
{
    [self.lock lock];

    // 由于大量port message在同时被发送时,可能会被丢弃,为了防止没有处理到,这里遍历数组来进行处理。
    while ([self.notifications count]) {
        NSNotification *notification = [self.notifications objectAtIndex:0];
        [self.notifications removeObjectAtIndex:0];
        [self.lock unlock];
        [self processNotification:notification];
        [self.lock lock];
    }

    [self.lock unlock];
}

- (void)processNotification:(NSNotification *)notification
{
    NSThread *ct = [NSThread currentThread];
    // 不是指定线程
    if (ct != _thread) {
        [self.lock lock];
        // 添加到队列
        [self.notifications addObject:notification];
        [self.lock unlock];

        // 通过mach port发送消息
        [self.port sendBeforeDate:[NSDate date] components:nil from:nil reserved:0];
    }else{
        NSLog(@"process notification %@,is main %zd",[NSThread currentThread],[NSThread isMainThread]);
    }
}

如果,我们想要在主线程中接收通知,在viewDidLoad中。

- (void)viewDidLoad {
      self.notificationHandler = [NotificationHandler new];

// 在子线程中post,会在主线程中收到
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"post notification %@,is main %zd",[NSThread currentThread],[NSThread isMainThread]);
        [[NSNotificationCenter defaultCenter] postNotificationName:@"notification" object:nil];

        [[NSNotificationCenter defaultCenter] postNotificationName:@"notification" object:nil];
    });
}

或者,需要在某个子线程中接收,可以这样。

- (void)viewDidLoad {
    [super viewDidLoad];

    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(startThread) object:nil];
    [self.thread start];
}

- (void)startThread {

    NSLog(@"startThread %@,is main %zd",[NSThread currentThread],[NSThread isMainThread]);

    self.notificationHandler = [NotificationHandler new];
    
    // 在另外的子线程中发送通知dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"post notification %@,is main %zd",[NSThread currentThread],[NSThread isMainThread]);
        [[NSNotificationCenter defaultCenter] postNotificationName:@"notification" object:nil];

        [[NSNotificationCenter defaultCenter] postNotificationName:@"notification" object:nil];
    });

    // 线程中需要自己启动runloop
    [[NSRunLoop currentRunLoop] run];
}

参考:
Notifications

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • 深入理解RunLoop 由ibireme| 2015-05-18 |iOS,技术 RunLoop 是 iOS 和 ...
    橙娃阅读 847评论 1 2
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • 从哪说起呢? 单纯讲多线程编程真的不知道从哪下嘴。。 不如我直接引用一个最简单的问题,以这个作为切入点好了 在ma...
    Mr_Baymax阅读 2,726评论 1 17
  • 每一个想让生活更有质量的人都关注了书画艺贰叁 画画对她而言, 不只是一种乐趣, 更是一种修行。 早稻-野獸 201...
    书画艺贰叁阅读 500评论 0 1
  • 歲歲思君眼欲穿,年年醉等夢不還。 獨憐長夜不安睡,燈火闌珊只似閑。 恨離去,幾千般,月星高照相知難。 今生敢問何時...
    离离陌上草阅读 203评论 0 3