iOS 中的 timer 任务(寻找内存恶鬼之旅)

前言

     在 iOS 的开发过程中定时任务中能找到使用的场景,然而在 iOS 中默认的有关 timerapi 总是那么晦涩难用,而且暗坑不断,一旦遇上,会让你一脸懵逼,为了不再同一个地方跌倒两次,我决心花些时间做一篇总结,也用以提醒读者,谨慎使用。
     之前在做一个空白页的计时器的时候使用到了 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 就可以检测是否内存图关系:


这是 RunLoopTimer 的内存关系,再看看 Timertarget 的关系:

target_timer.png

方案

      既然这个环用常规的方法无法打破,那该怎么办呢? 这时候 NSProxy 就可也发挥它的长处了。我们实现一个 NSProxy 的子类 WeakProxyWeakProxy 弱引用一个 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 引用计数减一也会释放,因此,原来的环不在了,完美解决了相互引用的问题。

源码 Demo

你可能感兴趣的:(iOS 中的 timer 任务(寻找内存恶鬼之旅))