NSNotification 的便利性和内存泄露风险
实现在两个互不相关的模块之间通信,NSNotification是一个很好用的工具,但是觉得 NSNotification 的设计让开发者用起来不舒服。可以归结为不方便使用和有内存泄露的隐患问题。
- 不方便使用
NSNotificationCenter有以下的方法订阅通知。
- (void)addObserver:(id)notificationObserver
selector:(SEL)notificationSelector
name:(NSString *)notificationName
object:(id)notificationSender
selector的方式有两个不好的地方。
其一,订阅和处理订阅的逻辑分开,查看代码的时候不直观;
其二,需要再写一个 selector,和绞尽脑子想合适的名字,即使有些时候名字并不重要。
总的来说,代码不好组织和我懒。不过优点还是有的,不容易发生内存泄露。
- 内存泄露的隐患
NSNotificationCenter还提供一个方法实现订阅。
- (id<NSObject>)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 已经析构了。






网友评论