NSNotification 和 NStimer 的最佳实践

NSNotification 的便利性和内存泄露风险

实现在两个互不相关的模块之间通信,NSNotification是一个很好用的工具,但是觉得 NSNotification 的设计让开发者用起来不舒服。可以归结为不方便使用和有内存泄露的隐患问题。

  • 不方便使用
    NSNotificationCenter 有以下的方法订阅通知。
  - (void)addObserver:(id)notificationObserver
          selector:(SEL)notificationSelector 
          name:(NSString *)notificationName
          object:(id)notificationSender

selector的方式有两个不好的地方。
其一,订阅和处理订阅的逻辑分开,查看代码的时候不直观;
其二,需要再写一个 selector,和绞尽脑子想合适的名字,即使有些时候名字并不重要。
总的来说,代码不好组织和我懒。不过优点还是有的,不容易发生内存泄露。

  • 内存泄露的隐患
    NSNotificationCenter 还提供一个方法实现订阅。
  - (id)addObserverForName:(NSString *)name 
                  object:(id)obj 
                  queue:(NSOperationQueue *)queue 
                  usingBlock:(void (^)(NSNotification *note))block

处理订阅的逻辑写在 block 内,这样一来,selector 的缺点不存在了,逻辑相关的代码放在一块,不需要绞尽脑汁想方法名。

但是!会有内存泄露的风险,下面的代码有内存泄露问题。

id observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"Test1"
                                                      object:nil 
                                                      queue:nil 
                                                      usingBlock:^(NSNotification * _Nonnull note) {
                                                             NSLog(@"%@", self);
                                                      }];

NSNotificationCenter 强引用 observerblock 要是强引用 self,就会出现内存泄露。因此要避免内存引用,要在合适的地方取消订阅以及弱引用 self
比较 selectorblock 的方式,我倾向使用 block 的方案,写起来非常便捷。于是乎,方向只有一个,解决内存泄露的问题,禁止在 block 内强引用self

最佳实践

将焦点放在 usingBlock 里,要是能改成 usingBlock:^(id self, NSNotification * _Nonnull note),其中 id self 是弱引用 self,并在 block 的作用域内屏蔽外部的 self,从而实现将强引用的 self 替换成弱引用的 self

思路有了,就可以动手封装了,将系统的方法封装成带弱引用的 selfblockPLAPubSub 这个库只是把 selecotr 封装成 block 的调用方式,并没有消除内存泄露的风险。我在这个库(链接)的基础上,改了他的代码,解决了内存泄露的问题,核心代码如下。

typedef void (^Handler)(id self, Event *event);

- (id)subscribe:(NSString *)name handler:(Handler)handler {
    __weak __typeof__(self) weakSelf = self;
    id observer =  [[NSNotificationCenter defaultCenter] addObserverForName:name object:nil queue:nil usingBlock:^(NSNotification *note) {
        GLEvent *event = [[GLEvent alloc] initWithName:eventName obj:note.object data:[note.userInfo objectForKey:kGLPubSubDataKey]];
        handler(weakSelf, event);
    }];
    // 还要保存 observer,取消订阅的时候需要用到 observer,可以使用关联对象的方法存放 observer。
    return observer;
}

提醒一句,还是要正确使用,在 dealloc 里记得取消订阅,不然 NSNotificationCenter 不会释放 observer


NSTimer 的内存泄露风险

NSTimer 使用不当也是非常容易有内存泄露的问题。官方文档给出了 NSTimer 的 API,有一个特点。


+ scheduledTimerWithTimeInterval:invocation:repeats:
+ scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
+ timerWithTimeInterval:invocation:repeats:
+ timerWithTimeInterval:target:selector:userInfo:repeats:

很明显,需要有一个 invocation 或者 targetselector 的组合。而这是容易带来风险的地方。

 self.timer = [NSTimer scheduledTimerWithTimeInterval:5
                                                target:self
                                              selector:@selector(test)
                                              userInfo:nil
                                               repeats:NO];

这段代码已经产生了内存泄露,只有在5秒后才解除风险,如果把 repeats改为 YES,永远解除不了风险。有一个地方值得注意,无论 selftimer 是强引用还是若引用,都改变不了什么。

要弄清楚问题,需要了解 selfNSTimerRunLoop 是之间是如何引用的。

  • NSTimerRunLoop 的关系
    定时器的功能是借助 RunLoop 实现的,在 NSTimer 被加到 RunLoop 的时候,RunLoop 会强引用 NSTimer
    在被移除 RunLoop 的时候(类方法创建且不是重复或者调用 invalidate ),RunLoop 就不会再强引用 NSTimer

  • selfNSTimer 的关系
    selfNSTimer 的引用可强可弱。反过来,NSTimerself 只能是强引用。

再来看上一段代码,在前5秒,RunLoopNSTimer 是强引用,而 NSTimerself 只能是强引用,所以无论 selfNSTimer 是强还是弱引用,都不能析构 self,从而有了内存泄露的问题。

最佳实践

前面也提到,个人不喜欢 selector 和原因,在这里也是希望能通过封装,把 selector 封装成 block 的方式。我前同事 callMeWhy 开源了一个库 NSWeakTimer(链接),完美解决这个问题,代码也非常简单,我给大家讲解一下他的思路。

RunLoopTimer 的引用只能是强引用,Timerself 也只能是强引用,问题的解决方法是让 Timerself 弱引用,所以加入第三方,Timer 对第三方强引用, 第三方对 self 是弱引用,其余的地方能用弱引用都用弱引用。

NSNotification 和 NStimer 的最佳实践_第1张图片
引用关系图

最后提醒

我司开发没有正确使用 NOtificationNSTimer 导致出现了一些奇怪的问题,所以务必dealloc 取消订阅和停止计时器,不然 block 还是会执行,只是 self 已经析构了。

你可能感兴趣的:(NSNotification 和 NStimer 的最佳实践)