什么叫自释放?可以简单的理解为:对象在生命周期结束后,自动清理回收与其相关的资源。这个清理不仅仅包括对象内存的回收,还包括对象解耦及附属事件的清理等等,例如定时器的停止、通知以及 KVO 对象的监听移除。
对象内存的回收
在开发中,对象管理的基本原则 --- 谁创建谁释放。但是在 MRC 中,我们会用 autorelease
来标记一个对象,告诉编辑器,这个对象我不负责释放。此时,这个对象就变成了自释放的对象,当其不再需要时,系统就会自动回收其内存。 等到了 ARC 时代,基本上所有对象对于我们来说都是自释放对象,我们不需要再处处留意内存泄漏问题,可以更专注于业务逻辑上。
KVO 的自释放
iOS 开发中,我们使用 KVO 监听对象某个 keyPath
时,需要在被监听的对象释放前移除对应的 keyPath
监听:
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"];
}
复制代码
如果我们一不小心忘了移除对应的监听,会得到这样的错误:
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 也许会是一个不错的选择:
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 的自释放:
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 的自释放
通常,我们使用通知时是这样的:
// 添加
[[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 及以上版本中,我们已经不需要再手动移除通知了,大家可以用以下代码测试下:
@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 的自释放
通常我们是这样使用定时器:
@property (strong, nonatomic) NSTimer *timer;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
复制代码
定时器内部会 strong
target
,而 self
也就是 target
又 strong
了定时器,这样就造成了循环引用,导致 self
无法释放。想要打破,我们只有主动调用 invalidate
方法。目前解决这种问题的方法有两种方式:
- 使用
weak proxy
,持有弱引用target
,转发消息到target
。YYWeakProxy 是个不错的选择。 - 使用
dispatch_source
自己实现一个定时器。YYTimer 是个不错的选择。
YYWeakProxy
YYWeakProxy 是 NSProxy 的子类,其内持有了 weak
target
,利用消息转发机制,将消息转发到传进来的 target
:
@property (nullable, nonatomic, weak, readonly) id target;
复制代码
这样,当 self
引用计数为 0
时,target
将为 nil
,这样就打破了 self
和 NSTimer
之间的循环引用,self
也就得以释放。
然而,虽然 self
和 NSTimer
之间循环引用打破了,却又造成了 YYWeakProxy
和 NSTimer
之间的循环引用,导致 YYWeakProxy
的内存泄漏。按照作者的意思,与其泄漏一个可能很重的 self
,不如泄漏一个轻量的 YYWeakProxy
。
YYTimer
YYTimer 可以彻底的解决内存泄漏问题,缺点是实现相对复杂。 其内部是使用 GCD 的 dispatch_source
来实现的,关于 dispatch_source
使用如下:
// 队列
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);
复制代码
需要取消的话:
dispatch_source_cancel(self.timer);
复制代码