NSTimer 是 iOS 上的一種計時器,通過 NSTimer 對象,可以指定時間間隔,向一個對象發送消息。NSTimer 是比較常用的工具,比如用來定時更新界面,定時發送請求等等。但是在使用過程中,有很多需要注意的地方,稍微不注意就會產生 bug,crash,內存泄漏。本文講解了使用 NSTimer 時需要注意的問題。
需要注意的是,這種無限循環的 timer,會一直執行,需要調用
那,
有些人會在 self 的 dealloc 里面調用,這幾乎可以確定是錯誤的。因為 timer 會引用住 self,在 timer 停止之前,是不會釋放 self 的,self 的 dealloc 也不可能會被調用。
正確的做法應該是根據業務需要,在適當的地方啟動 timer 和 停止 timer。比如 timer 是頁面用來更新頁面內部的 view 的,那可以選擇在頁面顯示的時候啟動 timer,頁面不可見的時候停止 timer。比如:
錯誤特征 1:
錯誤特征 2:
錯誤特征 3:
Weak Timer 的實現方式分為兩種,第一種是在 NSTimer 和 target 中間加多一層代理(Proxy),代理作為 target 被 NSTimer 強引用,同時弱引用真正的 target,并對它轉發消息。示例圖如下:
比如這個: https://github.com/mindsnacks/MSWeakTimer。
1. NSTimer 容易泄漏
比如以下代碼創建了一個計時器:self.timer =[NSTimer scheduledTimerWithTimeInterval:1target:selfselector:@selector(update)userInfo:nilrepeats: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:1target:selfselector:@selector(update)userInfo:nilrepeats:YES];
}- (void)viewDidDisappear
{[super viewDidDisappear];[self.timer invalidate];
}
2. 錯誤特征
實際開發中,或者 Code Review 的時候,可以通過一些特征初步判定可能會有問題。錯誤特征 1:
- (void)dealloc
{[self.timer invalidate];
}
以上代碼是有問題的。當 timer 沒有停止的時候,self 會被引用,也就沒有機會走到 dealloc。同時,代碼作者應該對 timer 沒有正確的認識,所以需要 review 整個 timer 的使用情況。
錯誤特征 2:
[NSTimer scheduledTimerWithTimeInterval:1target:selfselector:@selector(update)userInfo:nilrepeats:YES];
以上代碼創建了一個 timer,但是沒有保存起來,后續自然也沒有機會停止這個 timer。所以會導致 timer 泄漏。
錯誤特征 3:
- (void)viewDidAppear:(BOOL)animated
{[super viewDidAppear:animated];self.timer =[NSTimer scheduledTimerWithTimeInterval:1target:selfselector:@selector(update)userInfo:nilrepeats:YES];
}
以上代碼也是有問題的。因為我們要確保 timer 的創建和銷毀必須是成對調用,否則會發生泄漏。而對于 viewDidAppear 其實很難找到一個準確的與之成對的方法(跟 viewWillDisappear 和 viewDidDisappear 都不是成對調用的),這里就需要檢查 Timer 有沒有被重復創建和有沒有在適當的時機銷毀。
3. 停止 timer 可能會導致 self 對象銷毀
值得注意的是,調用[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 沒處理好。
4. Perform Delay
[NSObject performSelector:withObject:afterDelay:]
和
[NSObject performSelector:withObject:afterDelay:inMode:]
我們簡稱為 Perform Delay,他們的實現原理就是一個不循環(repeat 為 NO)的 timer。所以使用這兩個接口的注意事項跟使用 timer 類似。需要在適當的地方調用
[NSObject
cancelPreviousPerformRequestsWithTarget:selector:object:]
5. Runloop Mode
注意創建 NSTimer 或者調用 Perform Delay 方法,都是往當前線程的 Runloop 中投遞消息,大部分接口的默認投遞模式是 CFRunloopDefaultMode。也就是說,Runloop 不在 DefaultMode 下運行的時候(比如滾動列表的時候主線程的 runloop mode 是 CFRunloopTrackingMode),消息將被暫時阻塞,不能及時處理。6. Weak Timer
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。