iOS 中的 timer 任務(尋找記憶體惡鬼之旅)

CNKCQ發表於2018-10-21

前言

     在 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 就可以檢測是否記憶體圖關係:

iOS 中的 timer 任務(尋找記憶體惡鬼之旅)
這是 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

相關文章