聊聊 iOS 中的自释放

什么叫自释放?可以简单的理解为:对象在生命周期结束后,自动清理回收与其相关的资源。这个清理不仅仅包括对象内存的回收,还包括对象解耦及附属事件的清理等等,例如定时器的停止、通知以及 KVO 对象的监听移除。

对象内存的回收

在开发中,对象管理的基本原则 — 谁创建谁释放。但是在 MRC 中,我们会用 autorelease 来标记一个对象,告诉编辑器,这个对象我不负责释放。此时,这个对象就变成了自释放的对象,当其不再需要时,系统就会自动回收其内存。 等到了 ARC 时代,基本上所有对象对于我们来说都是自释放对象,我们不需要再处处留意内存泄漏问题,可以更专注于业务逻辑上。

KVO 的自释放

iOS 开发中,我们使用 KVO 监听对象某个 keyPath 时,需要在被监听的对象释放前移除对应的 keyPath 监听:

1
2
3
4
5
6
7
Person *person = [Person new];
self.person = person;
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"name"];
}

如果我们一不小心忘了移除对应的监听,会得到这样的错误:

1
2
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x17000c2c0 of class Person was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x17003c9e0>(
<NSKeyValueObservance 0x170243de0: Observer: 0x129d053b0, Key path: name, Options: <New: YES, Old: YES, Prior: NO> Context: 0x0, Property: 0x170243db0>)'

FBKVOController

我们不由的产生疑问: 对象的 dealloc 函数只做了removeObserver:forKeyPath: 一件事,能不能不每次都写呢?FBKVOController 也许会是一个不错的选择:

1
2
3
4
5
6
7
8
Person *person = [Person new];
self.person = person;
[self.KVOController observe:person keyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
NSString *new = change[NSKeyValueChangeNewKey];
NSString *old = change[NSKeyValueChangeOldKey];
NSLog(@"%@ %@",new,old);
}];

抛开烦人的 removeObserver:forKeyPath:,更加简明清晰的满足了需求。

那么,FBKVOController 是如何做到自释放的呢?其内部将观察者绑定到 FBKVOController 这个第三者上,FBKVOController 会随着观察者的释放而释放。最后,FBKVOController 在自己的 dealloc 方法中,通过 _FBKVOSharedController 这个单例来移除监听。

ReactiveCocoa

除了 FBKVOController,ReactiveCocoa 也同样支持 KVO 的自释放:

1
2
3
4
5
6
Person *person = [Person new];
self.person = person;
[[self.person rac_valuesAndChangesForKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew observer:self] subscribeNext:^(RACTwoTuple<id,NSDictionary *> * _Nullable x) {
NSLog(@"%@ %@",x.second[@"old"],x.second[@"new"]);
}];

ReactiveCocoa 和 FBKVOController 略有不同,ReactiveCocoa 是通过监听观察者的 dealloc 方法,并通过 RACKVOTrampoline 这个对象来管理对象 KVO 监听的添加/移除。

⚠️ 经测试,在 iOS 11 中,系统已经帮我们做了 KVO 的 keyPath 移除操作。遗憾的是,iOS 11 以下,不移除仍然存在问题!

NSNotification 的自释放

通常,我们使用通知时是这样的:

1
2
3
4
5
6
// 添加
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(respondsToNotification:) name:@"test0" object:nil];
// 发送
[[NSNotificationCenter defaultCenter] postNotificationName:@"test0" object:nil];
// 移除
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"test0" object:nil];

关于移除操作,根据不同的业务场景,有的是放在 dealloc 方法中,有的是 viewWillDisappear: 方法中。然而,在 iOS 8 及以上版本中,我们已经不需要再手动移除通知了,大家可以用以下代码测试下:

1
2
3
4
5
6
7
8
9
10
11
@implementation NSNotificationCenter (NS)
+ (void)load {
Method origin = class_getInstanceMethod([self class], @selector(removeObserver:));
Method current = class_getInstanceMethod([self class], @selector(_removeObserver:));
method_exchangeImplementations(origin, current);
}
- (void)_removeObserver:(id)observer {
NSLog(@"调用移除通知方法: %@", observer);
}
@end

这应该是苹果在 iOS 11 中的一次优化。

NSTimer 的自释放

通常我们是这样使用定时器:

1
2
3
@property (strong, nonatomic) NSTimer *timer;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(timerTest) userInfo:nil repeats:YES];

定时器内部strong target,而 self 也就是 targetstrong 了定时器,这样就造成了循环引用,导致 self 无法释放。想要打破,我们只有主动调用 invalidate 方法。目前解决这种问题的方法有两种方式:

  • 使用 weak proxy,持有弱引用 target ,转发消息到 targetYYWeakProxy 是个不错的选择。
  • 使用 dispatch_source 自己实现一个定时器。YYTimer 是个不错的选择。

YYWeakProxy

YYWeakProxy 是 NSProxy 的子类,其内持有了 weak target,利用消息转发机制,将消息转发到传进来的 target

1
@property (nullable, nonatomic, weak, readonly) id target;

这样,当 self 引用计数为 0 时,target 将为 nil,这样就打破了 selfNSTimer 之间的循环引用,self 也就得以释放。

然而,虽然 selfNSTimer 之间循环引用打破了,却又造成了 YYWeakProxyNSTimer 之间的循环引用,导致 YYWeakProxy 的内存泄漏。按照作者的意思,与其泄漏一个可能很重的 self,不如泄漏一个轻量的 YYWeakProxy

YYTimer

YYTimer 可以彻底的解决内存泄漏问题,缺点是实现相对复杂。 其内部是使用 GCD 的 dispatch_source 来实现的,关于 dispatch_source 使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 创建 dispatch_source
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 声明成员变量
self.timer = timer;
// 设置两秒后触发
dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC);
// 设置下次触发事件为 DISPATCH_TIME_FOREVER
dispatch_time_t nextTime = DISPATCH_TIME_FOREVER;
// 设置精确度
dispatch_time_t leeway = 0.1 * NSEC_PER_SEC;
// 配置时间
dispatch_source_set_timer(timer, startTime, nextTime, leeway);
// 回调
dispatch_source_set_event_handler(timer, ^{
// ...
});
// 激活
dispatch_resume(timer);

需要取消的话:

1
dispatch_source_cancel(self.timer);