你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> NSTimer 使用進階

NSTimer 使用進階

編輯:IOS開發基礎

NSTimer 是 iOS 上的一種計時器,通過 NSTimer 對象,可以指定時間間隔,向一個對象發送消息。NSTimer 是比較常用的工具,比如用來定時更新界面,定時發送請求等等。但是在使用過程中,有很多需要注意的地方,稍微不注意就會產生 bug,crash,內存洩漏。本文講解了使用 NSTimer 時需要注意的問題。

1. NSTimer 容易洩漏

比如以下代碼創建了一個計時器:

self.timer =
  [NSTimer scheduledTimerWithTimeInterval:1
                                   target:self
                                 selector:@selector(update)
                                 userInfo:nil
                                  repeats:YES];

上述代碼,將創建一個無限循環的 timer,並投入當前線程的 Runloop 中開始執行。此時,Runloop 會引用住 timer,timer 會引用住 self,self 則保存了 timer。如下圖所示:

20161012-1.png

需要注意的是,這種無限循環的 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];
}

2. 錯誤特征

實際開發中,或者 Code Review 的時候,可以通過一些特征初步判定可能會有問題。

錯誤特征 1:
- (void)dealloc
{
  [self.timer invalidate];
}

以上代碼是有問題的。當 timer 沒有停止的時候,self 會被引用,也就沒有機會走到 dealloc。同時,代碼作者應該對 timer 沒有正確的認識,所以需要 review 整個 timer 的使用情況。

錯誤特征 2:
[NSTimer scheduledTimerWithTimeInterval:1
                                 target:self
                               selector:@selector(update)
                               userInfo:nil
                                repeats:YES];

以上代碼創建了一個 timer,但是沒有保存起來,後續自然也沒有機會停止這個 timer。所以會導致 timer 洩漏。

錯誤特征 3:
- (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 有沒有被重復創建和有沒有在適當的時機銷毀。

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,並對它轉發消息。示例圖如下:

20161012-2.png

+ (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。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved