NSTimer 是 iOS 上的一種計時器,通過 NSTimer 對象,可以指定時間間隔,向一個對象發送消息。NSTimer 是比較常用的工具,比如用來定時更新界面,定時發送請求等等。但是在使用過程中,有很多需要注意的地方,稍微不注意就會產生 bug,crash,內存洩漏。本文講解了使用 NSTimer 時需要注意的問題。
比如以下代碼創建了一個計時器:
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(update) userInfo:nil repeats:YES];
上述代碼,將創建一個無限循環的 timer,並投入當前線程的 Runloop 中開始執行。此時,Runloop 會引用住 timer,timer 會引用住 self,self 則保存了 timer。如下圖所示:
需要注意的是,這種無限循環的 timer,會一直執行,需要調用[timer invalidate]
顯式停止。否則 runloop 會一直引用著 timer,timer 又引用了 self,導致 self 整個對象洩漏,實際情況中,這個 self 有可能是一個 view,甚至是一個 controller。
那,[timer invalidate]
要什麼時候調用?
有些人會在 self 的 dealloc 裡面調用,這幾乎可以確定是錯誤的。因為 timer 會引用住 self,在 timer 停止之前,是不會釋放 self 的,self 的 dealloc 也不可能會被調用。
正確的做法應該是根據業務需要,在適當的地方啟動 timer 和 停止 timer。比如 timer 是頁面用來更新頁面內部的 view 的,那可以選擇在頁面顯示的時候啟動 timer,頁面不可見的時候停止 timer。比如:
- (void)viewWillAppear { [super viewWillAppear]; self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(update) userInfo:nil repeats:YES]; } - (void)viewDidDisappear { [super viewDidDisappear]; [self.timer invalidate]; }
實際開發中,或者 Code Review 的時候,可以通過一些特征初步判定可能會有問題。
- (void)dealloc { [self.timer invalidate]; }
以上代碼是有問題的。當 timer 沒有停止的時候,self 會被引用,也就沒有機會走到 dealloc。同時,代碼作者應該對 timer 沒有正確的認識,所以需要 review 整個 timer 的使用情況。
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(update) userInfo:nil repeats:YES];
以上代碼創建了一個 timer,但是沒有保存起來,後續自然也沒有機會停止這個 timer。所以會導致 timer 洩漏。
- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(update) userInfo:nil repeats:YES]; }
以上代碼也是有問題的。因為我們要確保 timer 的創建和銷毀必須是成對調用,否則會發生洩漏。而對於 viewDidAppear 其實很難找到一個准確的與之成對的方法(跟 viewWillDisappear 和 viewDidDisappear 都不是成對調用的),這裡就需要檢查 Timer 有沒有被重復創建和有沒有在適當的時機銷毀。
值得注意的是,調用 [timer invalidate]
停止 timer,此時 timer 會釋放 target,如果 timer 是最後一個持有 target 的對象,那麼此次釋放會直接觸發 target 的 。比如:
- (void)onEnterBackground:(id)sender { [self.timer invalidate]; [self.view stopAnimation]; // dangerous! }
以上代碼,加入第一行的 invalidate 之後,self 被銷毀了,那麼第二行訪問 self.view 時候,就會觸發野指針 crash。因為 Objective-C 的方法裡面,self 是沒有被 retain 的。這種情況,有個臨時的解決方案如下:
- (void)onEnterBackground:(id)sender { __weak id weakSelf = self; [self.timer invalidate]; [weakSelf.view stopAnimation]; // dangerous! }
將 self 改為弱引用。但是也是一個臨時解決方案。正確解決方法是,查出其它對象沒有引用 self 的時候,為什麼 timer 還沒停止。這個案例告訴大家,當見到 invalidate 被調用之後很神奇地出現了 self 野指針 crash 的時候,不要驚訝,就是 timer 沒處理好。
[NSObject performSelector:withObject:afterDelay:]
和 [NSObject performSelector:withObject:afterDelay:inMode:]
我們簡稱為 Perform Delay,他們的實現原理就是一個不循環(repeat 為 NO)的 timer。所以使用這兩個接口的注意事項跟使用 timer 類似。需要在適當的地方調用 [NSObject
cancelPreviousPerformRequestsWithTarget:selector:object:]
注意創建 NSTimer 或者調用 Perform Delay 方法,都是往當前線程的 Runloop 中投遞消息,大部分接口的默認投遞模式是 CFRunloopDefaultMode。也就是說,Runloop 不在 DefaultMode 下運行的時候(比如滾動列表的時候主線程的 runloop mode 是 CFRunloopTrackingMode),消息將被暫時阻塞,不能及時處理。
NSTimer 之所以比較難用對,比較重要的原因主要是 NSTimer 對 target 是強引用的。這導致了 target 洩漏,或者生命周期超出開發者的預期。timer 如果對 target 是弱引用的話,這些問題就不存在了,這就是 Weak Timer。
Weak Timer 的實現方式分為兩種,第一種是在 NSTimer 和 target 中間加多一層代理(Proxy),代理作為 target 被 NSTimer 強引用,同時弱引用真正的 target,並對它轉發消息。示例圖如下:
+ (NSTimer *)qz_scheduledWeakTimerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats { QzoneWeakProxy *proxy = [[QzoneWeakProxy weakProxyForObject:target]; return [self scheduledTimerWithTimeInterval:ti target:proxy selector:aSelector userInfo:userInfo repeats:repeats]; }
第二種方案是用 dispatch timer 自己實現一遍 timer,具體實現裡面,弱引用 target。
比如這個:https://github.com/mindsnacks/MSWeakTimer。