iOS NSTimer的問題總結

NSTimer在時長開發中使用頻率仍是比較高的,但一個不注意可能就會形成了小問題,平常使用中還需多注意纔是。bash

NSTimer計時準確嗎

問:NSTimer計時準確嗎?app

答:NSTimer 若是以下使用通常不許確:oop

//在主線程中調用
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(logInfo) userInfo:nil repeats:YES];

//或者 在主線程中調用
 _timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(logInfo) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];

複製代碼

以上兩種方式直接調用NSTimer時極可能會計時不許確,緣由以下:測試

  1. NSTimer在每一次的runloop中會被處理,但當runloop中有其餘比較多的耗時操做,且操做時間超過了NSTimer的間隔,那麼這一次的NSTimer就會被延後處理。致使不許確。 解決辦法:能夠將NSTimer放入子線程,並手動開啓子線程的runloop。當前runloop中沒有其餘耗時操做,因此也會相對準確一些。ui

  2. runloop model 模式的影響,當沒有指定時,runloop默認會添加到RunLoopDefaultMode中,當頁面有tableview滑動時,主線程的runloop會切換到TrackingRunLoopMode,此模式下,NSTimer不會被觸發,致使計時不許確。spa

NSTimer 內存泄漏

NSTimer涉及到內存泄漏,主要是指在Timer中repeatsYES,即須要間隔指定時間,重複調用方法,此時須要手動調用[self.timer invalidate]使定時器失效。線程

若是repeatsNO,則不存在內存泄漏問題,調用完成後,定時器會自動失效。code

問題引起:

通常使用NSTimer時,用法以下:cdn

self.timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(doSomeThing) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
複製代碼

上面代碼這樣寫,定時器會正常調用,但當退出當前頁面或者功能以後,定時器依然在調用,並無中止。想要中止,須要手動調用[self.timer invalidate]方法。對象

可能想到下面這種寫法:

- (void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;
}
複製代碼

這種寫法咋一看沒有問題,不是要手動調用中止定時器的方法嘛,那在頁面釋放的時候,調用就行了。

通過測試,能夠發現這樣寫的話,當前對象self不會釋放,定時器也沒有釋放,這就形成了內存泄漏。

分析問題:

再看一下以前的代碼:

- (void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;
}

self.timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(doSomeThing) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
複製代碼

self持有了timer, 而timertarget參數裏持有了self

相互引用

這就致使了相互引用。

可能有的同窗立馬就有了疑惑:

  1. self不持有timer也能夠正常使用,爲何要持有
  2. timer爲何會持有self

針對以上兩個問題答疑:

  1. self持有timer是由於須要在其餘的地方手動調用[self.timer invalidate]self不持有timer確實能夠簡單解決相互引用問題,但timer卻沒法手動釋放。
  2. runloop須要對timer的觀察者作保留操做,以便後續指定的時間點來到時作指定操做。
解決問題

解決上面的內存泄漏問題,大體有下面幾種辦法:

  1. viewWillDisappear:手動中止
  2. 重寫返回方法手動中止
  3. iOS10以後,系統提供的scheduledTimerWithTimeInterval:repeats:block方法
  4. 去除NSTimer和當前調用對象之間的循環引用。

詳細解釋:

手動中止定時器,要有一個時機。若是根據需求,恰好能夠在用戶操做某項功能時能夠主動中止,算是一個比較好的辦法。但若是沒有這個時機則能夠在一、2兩個方法中解決。

  1. viewWillDisappear:手動中止

    - (void)viewWillDisappear:(BOOL)animated {
        [super viewWillDisappear:animated];
        [self.timer invalidate];
        self.timer = nil;
    }
    複製代碼

    須要注意在這個方法中中止,適用於不會從當前頁面push出下一個頁面的狀況,由於若是push出下一個頁面,也會調用viewWillDisappear:,可能就得不到正確的結果。

  2. 重寫返回方法手動中止

    - (void)backButtonPressed:(id)sender {
        [self.timer invalidate];
        self.timer = nil;
    }
    複製代碼
  3. 蘋果估計也發現了timer容易致使內存泄漏的問題,因此在iOS10以後,出了一個新的API,使用新API是沒有內存泄漏的。

    if (@available(iOS 10.0, *)) {
        self.timer = [NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"do more thing");
        }];
    }
    
    - (void)dealloc
    {
        [self.timer invalidate];
        self.timer = nil;
    }
    複製代碼

    調用這個新的API則不會相互強引用,dealloc方法會正常調用。

  4. 去除NSTimer和當前調用對象之間的循環引用。 從3中的系統方法其實能夠獲得啓發,3中的方法裏並無直接引用self,而是讓timer引用了其餘的對象,這樣就解除了相互引用。 對於iOS10一下的系統,能夠模仿一下系統的實現。

    #import <Foundation/Foundation.h>
    
     NS_ASSUME_NONNULL_BEGIN
    
     @interface NSTimer (weak)
    
     + (NSTimer *)weak_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void(^)(NSTimer *timer))block;
    
     @end
    
     NS_ASSUME_NONNULL_END
    複製代碼
    #import "NSTimer+weak.h"
    
    @implementation NSTimer (weak)
    
    + (NSTimer *)weak_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block{
         return [NSTimer scheduledTimerWithTimeInterval:inerval target:self selector:@selector(weak_blcokInvoke:) userInfo:[block copy] repeats:repeats];
    }
    
    + (void)weak_blcokInvoke:(NSTimer *)timer {
     
        void (^block)(NSTimer *timer) = timer.userInfo;
     
         if (block) {
             block(timer);
         }
     }
    
     @end
    複製代碼

    在使用時,能夠以下:

    self.timer = [NSTimer weak_scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
         //do some thing
    }];
     
    - (void)dealloc
     {
         [self.timer invalidate];
         self.timer = nil;
     }
    複製代碼

    能夠看把target由以前原始的vc對象,轉換成了timer對象,從而打破了雙向引用。

相關文章
相關標籤/搜索