聊聊 iOS 中的自释放

什么叫自释放?可以简单的理解为:对象在生命周期结束后,自动清理回收与其相关的资源。这个清理不仅仅包括对象内存的回收,还包括对象解耦及附属事件的清理等等,例如定时器的停止、通知以及 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 也就是 targetstrong 了定时器,这样就造成了循环引用,导致 self 无法释放。想要打破,我们只有主动调用 invalidate 方法。目前解决这种问题的方法有两种方式:

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

YYWeakProxy

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

@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 使用如下:

// 队列
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);
复制代码

参考

www.olinone.com/?p=232

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值