前言
在 iOS
的开发过程中定时任务中能找到使用的场景,然而在 iOS
中默认的有关 timer
的 api
总是那么晦涩难用,而且暗坑不断,一旦遇上,会让你一脸懵逼,为了不再同一个地方跌倒两次,我决心花些时间做一篇总结,也用以提醒读者,谨慎使用。
之前在做一个空白页的计时器的时候使用到了 CADisplayLink
,这货把我坑惨了, 循环引用导致内存随着时间的增加而上升,短时间使用没啥感觉,要不是使用工具这是很难发现的。
分析
通常,在解决循环引用的时候我们会引入 weak
, 通过 weak
修饰打破循环引用中的 环
, 如:
@property (nonatomic, weak) CADisplayLink *link;
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(fireAction)];
[self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
然而,这样做 link
直接不工作了, 因为 link
没有别的地方引用,当它初始化完成立即就被释放掉了 。那么换一种思路呢?
__weak typeof(self) weakSelf = self;
self.link = [CADisplayLink displayLinkWithTarget:weakSelf selector:@selector(fireAction)];
[self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
这样做也是徒劳的,当 self.link
持有 weakSelf
时也就是持有了 self
, 而 link
是通过 target
强持有的 self
所以还是无法打破形成的环,我们通过 Memory Graph
就可以检测是否内存图关系:
这是
RunLoop
与 Timer
的内存关系,再看看 Timer
与 target
的关系:
方案
既然这个环用常规的方法无法打破,那该怎么办呢? 这时候 NSProxy
就可也发挥它的长处了。我们实现一个 NSProxy
的子类 WeakProxy
,WeakProxy
弱引用一个 target
,然后在通过 WeakProxy
消息转发到 target
从而达到破除循环的效果:
- (id)forwardingTargetForSelector:(SEL)selector {
return _target;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
[invocation setReturnValue:&null];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
proxy
弱引用的 target
所以不影响 target
的正常释放,当 target
释放后,link
引用计数减一 link
释放,proxy
引用计数减一也会释放,因此,原来的环不在了,完美解决了相互引用的问题。