何为“自释放”?可以简单的理解为对象在生命周期结束后自动清理回收所有与其相关的资源或链接,这个清理不仅仅包括对象内存的回收,还包括对象解耦以及附属事件的清理等,比如定时器的自我停止、KVO对象的监听移除等
对象内存的回收
开发中,对象管理的基本原则——谁创建谁释放。但是,非ARC工程中,我们会用autorelease来标记一个对象,告诉编辑器,这个对象我不负责释放,此时,这个对象就变成了“自释放”对象,当其不再需要时,系统就会自动回收其内存。而ARC工程中,所有对象对于我们来说都是自释放对象,很高兴,我们不再需要处处留意内存泄露的问题,可以把更多的精力放在业务逻辑上,但是这并不意味着真的没有内存泄露,试试这个工具HJNSObjectRelease,也许你会有意想不到的收获。
定时器的自释放
定时器与一般对象不同,当创建完定时器后,其并不会自我释放,需要在适当时刻invalidate。在实际开发中,也许你经常会这样创建定时器
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(onTimerCount) userInfo:nil repeats:YES];
然后在dealloc函数中将定时器invalidate。很遗憾,你会发现程序永远也不会执行到dealloc函数,因为NSTimer强引用target对象,循环引用的出现必然导致内存泄露。此时,你肯定非常想要一个weak target的定时器,很高兴,MSWeakTimer很好的满足了你的需求。但是,Timer仍然没有自我释放,你仍然需要在dealloc中将其invalidate。那么,如何才能不写invalidate?定时器能否自释放?我们先把这个问题放在一边,接着往下看
KVO的自释放
iOS开发中,经常会用到消息通知及KVO,也许你会这样写代码
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onNotice) name:@"NoticeIdentifier" object:nil];
[self addObserver:target forKeyPath:@"keyPath" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"NoticeIdentifier" object:nil];
[self removeObserver:target forKeyPath:@"keyPath"];
}
随着时间的积累,你会非常习惯这种写法,并且苹果也是这样推荐的。但是慢慢你会发现所有对象的Dealloc函数都只做了这一件事,能不能不做这件事?FBKVOController也许会是一个不错的选择,Demo可以这样写
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {
clockView.date = change[NSKeyValueChangeNewKey];
}];
FBKVOController的实现原理可以查看这篇文章,通过自释放的实现,程序猿不再关心remove监听。但是其还是有一定的局限性——对象无法监听自己的属性,如果你的代码是这样的
[self.KVOController observe:self keyPath:@"date" options:NSKeyValueObservingOptionNew block:^(NSDictionary *change) {
// to do
}];
很遗憾,循环引用的问题又出现,因为FBKVOController中的NSMapTable对象会retain key对象,具体代码如下
[_objectInfosMap setObject:infos forKey:object];
那么,FBKVOController是如何做到自释放的?可以归纳为四个字——动态属性。其为观察者绑定动态属性self.KVOController,动态绑定的KVOController会随着观察者的释放而释放,KVOController在自己的dealloc函数中移除KVO监听,巧妙的将观察者的remove转移到其动态属性的dealloc函数中。
可是,这又有什么用?对象仍然无法监听自己的属性,还是要重写set函数。HTBKVObservation也许会改变你的想法,其和FBKVOController来自同一人,代码可以这样写
self.anObservation = [HTBKVObservation observe:anObjectToObserve keyPath:@"observeMe" options:0 callback:^(HTBKVObservation *observation, NSDictionary *changeDictionary) {
// to do
}];
HTBKVObservation并没用采用动态属性,而是采用属性的方式实现自释放。可以监控对象自己的属性,但是需要创建属性HTBKVObservation。 这里我对其做了一点扩展,方便使用,代码可以这样写
[self observe:self keyPath:@"KVOPath" options:NSKeyValueObservingOptionNew callback:^(HTBKVObservation *observation, NSDictionary *changeDictionary) {
// to do
}];
FBKVOController 和HTBKVObservation通过属性或动态属性巧妙的将KVO的remove转移给第三者,实现了KVO事件的解耦,为自释放的实现提供了一种借鉴思路
NSNotification的自释放
谈完 KVO,再来谈谈NSNotification。针对Notification,ReactiveCocoa做了很好的封装,网上有很多介绍其如何使用的文章,在此不再累述。直接看代码
[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardDidChangeFrameNotification object:nil] subscribeNext:^(id x) {
// to do
}
];
简单明了,当观察者dealloc,很遗憾,NSNotification并没用移除,因为对象并没用自释放,正确代码应该是这样
[[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardDidChangeFrameNotification object:nil] takeUntil:self.rac_willDeallocSignal] subscribeNext:^(id x) {
// to do
}
];
ReactiveCocoa自释放的原理与FBKVOController不同,其并不是通过属性或者动态属性的方式实现,而是通过swizzling观察对象的dealloc函数,在自定义dealloc函数实施清理,但不是默认清理,需要我们告诉它willDeallocSignal的时候完成所有清理工作。
除了定时器、KVO、NSNotification,包括封装的某个功能对象,比如HttpRequest,或者数据库ListSql等,合理的利用自释放可以给使用者带来更多的便利,同时也会减少 crash 产生的概率。实现自释放的方法可以总结为以下三种方式
- 动态属性的自释放
- @property 的自释放
- swizzling dealloc的自释放