NSTimer詳解----使用、保留環問題、與runloop的關係

@oneButterfly發表於2017-04-06

一、使用NSTimer你需要了解的內容
(1)只有將計時器放在執行迴圈中,它才能正常的觸發任務。
(2)NSTimer物件會保留target,直到計時器失效,呼叫invalidate可令其失效;一次性計時器觸發完就失效
(3)反覆執行的timer容易造成保留環。
(4)可以使用分類,用block打破保留環,後面會具體介紹
iOS 10之後引入新方法,可以得到timer弱引用避免保留環

__weak MyClass* weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer* t) {
    MyClass* _Nullable strongSelf = weakSelf;
    [strongSelf doSomething];
}];

(5)NSTimer的實時性
無論是單次執行的NSTimer還是重複執行的NSTimer都不是準時的,這與當前NSTimer所處的執行緒有很大的關係,如果NSTimer當前所處的執行緒正在進行大資料處理(假設為一個大迴圈),NSTimer本次執行會等到這個大資料處理完畢之後才會繼續執行。

這期間有可能會錯過很多次NSTimer的迴圈週期,但是NSTimer並不會將前面錯過的執行次數在後面都執行一遍,而是繼續執行後面的迴圈,也就是在一個迴圈週期內只會執行一次迴圈。

無論迴圈延遲的多離譜,迴圈間隔都不會發生變化,在進行完大資料處理之後,有可能會立即執行一次NSTimer迴圈,但是後面的迴圈間隔始終和第一次新增迴圈時的間隔相同。

二、相關API介紹

//使用下面兩個方法建立timer,你需要手動顯示將timer新增進自定義的runloop中
timerWithTimeInterval:invocation:repeats:
timerWithTimeInterval:target:selector:userInfo:repeats

//使用下面兩個函式,timer會預設新增到當前runloop或者預設runloop中
scheduledTimerWithTimeInterval:invocation:repeats:
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
//這裡分析最後一個函式:這個方法建立出來的計時器可以在一段時間後執行任務,也可以令其重複執行任務;計時器會自動保留物件,等到任務執行完畢再釋放物件。呼叫invalidate可以使timer失效;執行完任務後,一次性的timer會失效。如果將計時器設定為重複執行的模式,必須手動呼叫invalidate使其停止

//觸發timer
- (void)fire;

//使timer無效
(void)invalidate;

@property (copy) NSDate *fireDate;
@property (readonly) NSTimeInterval timeInterval;


//下面是iOS10之後引入的方法
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));//新增到當前runloop的預設model中
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

三、簡單使用

@interface
    NSTimer *autoTimer;

@implementation

// Start timer
    autoTimer = [NSTimer scheduledTimerWithTimeInterval:(3.0)
        target:self 
        selector:@selector(autoTimerFired:) 
        userInfo:nil 
        repeats:YES];

// Stop timer:
    [autoTimer invalidate];
    autoTimer = nil;

四、timer與runloop
(1)timer為什麼要加入runloop?
NSTimer其實也是一種資源,如果看過多執行緒變成指引文件的話,我們會發現所有的source如果要起作用,就得加到runloop中去。同理timer這種資源要想起作用,那肯定也需要加到runloop中才會又效嘍。如果一個runloop裡面不包含任何資源的話,執行該runloop時會立馬退出。你可能會說那我們APP的主執行緒的runloop我們沒有往其中新增任何資源,為什麼它還好好的執行。我們不新增,不代表框架沒有新增,如果有興趣的話你可以列印一下main thread的runloop,你會發現有很多資源。

NSTimer例項總是需要在執行迴圈上進行排程才能正常執行。如果您從主執行緒中執行此操作,則可以使用scheduleTimerWithTimeInterval,它將自動新增到主執行迴圈,無需手動呼叫NSRunLoop方法addTimer。如果需要的話,可以建立計時器並自行新增。
如果您從後臺執行緒建立一個沒有自己的執行迴圈的計時器(預設情況下,當您使用後臺排程佇列或執行佇列時,正在執行的執行緒將不具有自己的執行迴圈)然後您必須手動將計時器新增到執行迴圈。

或者,如果您真的希望計時器在後臺執行緒上執行,而不是為該執行緒建立執行迴圈並將計時器新增到新的執行迴圈,則可以使用GCD排程計時器,它不需要runloop。有關Objective-C示例
簡單的英文大家應該可以理解的……^_^
If you want a repeating timer to be invoked on a dispatch_queue_t, use dispatch_source_create with a DISPATCH_SOURCE_TYPE_TIMER:

dispatch_queue_t  queue = dispatch_queue_create("com.firm.app.timer", 0);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), 20ull * NSEC_PER_SEC, 1ull * NSEC_PER_SEC);

dispatch_source_set_event_handler(timer, ^{

    // stuff performed on background queue goes here

    NSLog(@"done on custom background queue");

    // if you need to also do any UI updates, synchronize model updates,
    // or the like, dispatch that back to the main queue:

    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"done on main queue");
    });
});

dispatch_resume(timer);

That creates a timer that runs once every 20 seconds (3rd parameter to dispatch_source_set_timer), with a leeway of a one second (4th parameter to dispatch_source_set_timer).
To cancel this timer, use dispatch_source_cancel:
dispatch_source_cancel(timer);

因此,除非在後臺執行緒中建立定時器,否則只需使用scheduledTimerWithTimeInterval,而不必擔心手動將其新增到執行迴圈中

(2)runloop 的mode的切換對timer的影響
以實際例子說明

// 0.沒有設定runloop模式的方式  
//    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];  

// 1.建立NSTimer  
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];  

// 2.1.新增到runloop  
// 把定時器新增到當前的runloop中,並選擇預設執行模式  
// kCFRunLoopDefaultMode == NSDefaultRunLoopMode  
// 但是這種模式下如果拖拽介面,runloop會自動進入UITrackingMode,優先於定時器追蹤模式  
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];  

// 2.2.我們更改一下模式UITrackingRunLoopMode  
// 當runloop的模式是UITrackingRunLoopMode時定時器才工作  
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];  

// 2.3.還有一種runloop的mode,佔位執行模式  
// 就可以無論是介面追蹤還是普通模式都可以執行  
/** 
 common modes = <CFBasicHash 0x7fb7424021b0 [0x10f12f7b0]>{type = mutable set, count = 2, 
 entries => 
 0 : <CFString 0x11002a270 [0x10f12f7b0]>{contents = "UITrackingRunLoopMode"} 
 2 : <CFString 0x10f14fb60 [0x10f12f7b0]>{contents = "kCFRunLoopDefaultMode"} 
 */  

/** 
 NSTimer的問題,NSTimer是runloop的一個觸發源,由於NSTimer是新增到runloop中使用的,所以如果只新增到default模式,會導致拖拽介面的時候runloop進入介面跟蹤模式而定時器暫停執行,不拖拽又恢復的問題,這時候就應該使用runloop的NSRunLoopCommonModes模式,能夠讓定時器在runloop兩種模式切換時也不受影響。 
 */  
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];  

五、NSTimer保留環問題

    1   @interface NSTimer (EOCBlocksSupport)  
    2     
    3   + (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval  
    4                                             block:(void(^)())block  
    5                                           repeats:(BOOL)repeats;  
    6     
    7   @end  
    8     
    9   @implementation NSTimer (EOCBlocksSupport)  
    10    
    11  + (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval  
    12                                            block:(void(^)())block  
    13                                          repeats:(BOOL)repeats  
    14  {  
    15      return [self scheduledTimerWithTimeInterval:interval  
    16                                           target:self  
    17                                         selector:@selector(eoc_blockInvoke:)  
    18                                         userInfo:[block copy]  
    19                                          repeats:repeats];  
    20  }  
    21    
    22  + (void)eoc_blockInvoke:(NSTimer*)timer {  
    23      void (^block)() = timer.userInfo;  
    24      if (block) {  
    25          block();  
    26      }  
    27  } 

在xxxViewContoler裡面使用這個擴充套件。

-(void)startPolling {

 __weak EOCClass * weakself  = self;

  _pollTimer =  eoc_scheduledTimerWithTimeInterval:5.0  
     block:^ {
    EOCClass *strongSelf = weakself;
    //上邊程式碼也可以用__strong來修飾強關係,這裡面應該是變為autoReleaseing的了。可以保證在這個作用於範圍使用。
    [strongSelf doReFresh];
    }  
   repeats:(BOOL)YES;
}

此處使用__weak還能讓程式更加安全,倘若開發者在delloc的時候忘記呼叫invalidate,從而使定時器在執行【這裡就有疑問了,xxxcontroller持有timer,當控制器回收時timer也應該引用數為0啊,原因是這裡runloop還會持有一份引用。】,倘若這樣的事情發生,weakself會變為nil。


下面整理的stack overflow上的問題,對於理解NSTimeryou

疑問
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:self selector:@selector(tick) userInfo:nil repeats:YES];
這句程式碼,NSTimer將保留target造成保留環,(如果使用timer的例項為A,當指向A例項的最後一個引用移走後,A例項不會被銷燬因為timer還保留著它(這時timer重複執行),而timer也不會釋放因為A引用著它,所以A例項永久的存在,也就是記憶體洩漏了),我瞭解timer必須invalidated,那麼我在dealloc方法中停用timer可以避免,對嗎?

解答1:在dealloc中無效定時器是無用的(在這裡):定時器保持對其目標的強烈引用。這意味著只要定時器保持有效,其目標將不會被釋放。作為推論,這意味著定時器的目標在其dealloc方法中嘗試使定時器無效是沒有意義的,只要定時器有效,dealloc方法將不被呼叫。

針對解答2:
__weak id weakSelf = self;
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:weakSelf selector:@selector(tick) userInfo:nil repeats:YES];

具有以下作用:(i)對self有一個弱應用; (ii)讀取弱引用來提供指向NSTimer的指標。 它不會產生具有弱引用的NSTimer的效果。 該程式碼和使用__strong引用之間的唯一區別是,如果self被釋放,那麼你將傳遞nil給定時器。

擴充---stack overflow上提供的一種使用GCD的API事項的重複執行

- (void) doSomethingRepeatedly
{
    // Do it once
    NSLog(@"doing something …");

    // Repeat it in 2.0 seconds
    __weak typeof(self) weakSelf = self;
    double delayInSeconds = 2.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        [weakSelf doSomethingRepeatedly];
    });
}

相關文章