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
强引用 observer
,block
要是强引用 self
,就会出现内存泄露。因此要避免内存引用,要在合适的地方取消订阅以及弱引用 self
。
比较 selector
和 block
的方式,我倾向使用 block
的方案,写起来非常便捷。于是乎,方向只有一个,解决内存泄露的问题,禁止在 block
内强引用self
。
最佳实践
将焦点放在 usingBlock
里,要是能改成 usingBlock:^(id self, NSNotification * _Nonnull note)
,其中 id self
是弱引用 self
,并在 block
的作用域内屏蔽外部的 self
,从而实现将强引用的 self
替换成弱引用的 self
。
思路有了,就可以动手封装了,将系统的方法封装成带弱引用的 self
的 block
。PLAPubSub
这个库只是把 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
或者 target
和 selector
的组合。而这是容易带来风险的地方。
self.timer = [NSTimer scheduledTimerWithTimeInterval:5
target:self
selector:@selector(test)
userInfo:nil
repeats:NO];
这段代码已经产生了内存泄露,只有在5秒后才解除风险,如果把 repeats
改为 YES
,永远解除不了风险。有一个地方值得注意,无论 self
对 timer
是强引用还是若引用,都改变不了什么。
要弄清楚问题,需要了解 self
、NSTimer
和 RunLoop
是之间是如何引用的。
NSTimer
和RunLoop
的关系
定时器的功能是借助RunLoop
实现的,在NSTimer
被加到RunLoop
的时候,RunLoop
会强引用NSTimer
。
在被移除RunLoop
的时候(类方法创建且不是重复或者调用invalidate
),RunLoop
就不会再强引用NSTimer
。self
和NSTimer
的关系
self
对NSTimer
的引用可强可弱。反过来,NSTimer
对self
只能是强引用。
再来看上一段代码,在前5秒,RunLoop
对 NSTimer
是强引用,而 NSTimer
对 self
只能是强引用,所以无论 self
对 NSTimer
是强还是弱引用,都不能析构 self
,从而有了内存泄露的问题。
最佳实践
前面也提到,个人不喜欢 selector
和原因,在这里也是希望能通过封装,把 selector
封装成 block
的方式。我前同事 callMeWhy 开源了一个库 NSWeakTimer
(链接),完美解决这个问题,代码也非常简单,我给大家讲解一下他的思路。
RunLoop
对 Timer
的引用只能是强引用,Timer
对 self
也只能是强引用,问题的解决方法是让 Timer
对 self
弱引用,所以加入第三方,Timer
对第三方强引用, 第三方对 self
是弱引用,其余的地方能用弱引用都用弱引用。
最后提醒
我司开发没有正确使用 NOtification
和 NSTimer
导致出现了一些奇怪的问题,所以务必在 dealloc
取消订阅和停止计时器,不然 block
还是会执行,只是 self
已经析构了。