iOS Timer 盤點

博文地址:http://ifujun.com/ios-timer-pan-dian/html

在iOS的開發過程當中,Timer是一個很常見的功能。蘋果提供給了咱們好幾種能夠達到Timer效果的方法,我嘗試在這裏盤點一下。ios

NSTimer

NSTimer是咱們最多見的一種Timer,咱們從NSTimer開始提及。git

用法

NSTimer的用法很簡單,我的比較經常使用的是下面這個方法:github

[NSTimer scheduledTimerWithTimeInterval:1.0f
                                     target:self
                                   selector:@selector(test)
                                   userInfo:nil
                                    repeats:nil];

Tips

爲什麼中止?

有這麼一道面試題,題目是這樣的:面試

UITableViewCell上有個UILabel,顯示NSTimer實現的秒錶時間,手指滾動cell過程當中,label是否刷新,爲何?數據結構

咱們來試驗一下:多線程

經過試驗,咱們發現,在手拖拽或者滑動的過程當中,label並無更新,NSTimer也沒有循環。app

那這是爲何呢?這與RunLoop有關。在NSTimer的官方文檔上,蘋果是這麼說的:框架

A timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed.less

意思就是說,NSTimer並非一種實時機制,它只會在下面條件知足的狀況下才會啓動:

  1. NSTimer被添加到的RunLoop模式正在運行。

  2. NSTimer設定的啓動時間尚未過去。

關鍵在於第一點,咱們剛纔的NSTimer默認添加在NSDefaultRunLoopMode上,而UIScrollView在滑動的時候,RunLoop會自動切換到 UITrackingRunLoopModeNSTimer並無添加到這個RunLoop模式上,天然是不會啓動的。

因此,若是咱們想要NSTimerUIScrollView滑動的時候也會啓動的話,只要將NSTimer添加到NSRunLoopCommonModes上便可。NSRunLoopCommonModes是RunLoop模式的集合。

咱們試驗一下:

self.timer = [NSTimer scheduledTimerWithTimeInterval:0.8f
                                              target:self
selector:@selector(autoIncrement)
                                            userInfo:nil
                                             repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

是否精確?

在上面的爲什麼中止的分析中,咱們瞭解到,NSTimer只會在它所加入的RunLoop上啓動和循環。若是在相似於上面的狀況,而NSTimer又只是加入NSDefaultRunLoopMode的話,這時候的NSTimer在切換RunLoop模式以後,必然沒有精確可言。

那麼,若是RunLoop一直運行在NSTimer被加入到的模式上,或者加入到的是NSRunLoopCommonModes的模式上,是否就是精確的呢?

首先,咱們假設線程正在進行一個比較大的連續運算,這時候,咱們的NSTimer會被準時啓動嗎?

我在程序中每0.5秒打印一下」test」,而後用很慢的辦法去計算質數,運行結果以下:

在計算質數的過程當中,線程徹底阻塞,並不打印」test」,等到執行完成纔開始打印,第一個」start」和第二個」test」中間間隔了13秒。

因此NSTimer是否精確,很大程度上取決於線程當前的空閒狀況。

除此以外,還有一點我想說起一下。

在NSTimer的頭文件中,蘋果新增了一個屬性,叫作tolerance,咱們能夠理解爲容差。蘋果的意思是若是設定了tolerance值,那麼:

設定時間 <= NSTimer的啓動時間 <= 設定時間 + tolerance

那麼,這個有什麼用呢,由於通常來講,咱們想要的就是精確。蘋果的解釋是:

Setting a tolerance for a timer allows it to fire later than the scheduled fire date, improving the ability of the system to optimize for increased power savings and responsiveness.

意思就是,設定容差能夠起到省電和優化系統響應性的做用。

tolerance若是不設定的話,默認爲0。那麼,是否必定能夠精確呢?蘋果在頭文件中提到了這麼一點:

The system reserves the right to apply a small amount of tolerance to certain timers regardless of the value of this property.

意思就是,哪怕爲0,系統依然有權利去設置一個很小的容差。

Even a small amount of tolerance will have a significant positive impact on the power usage of your application.

畢竟一個很小的容差均可以對電量產生一個很大的積極的影響。

因此,從上面的論述中咱們能夠看到,即便RunLoop模式正確,當前線程並不阻塞,系統依然可能會在NSTimer上加上很小的的容差。

如何終止?

NSTimer提供了一個invalidate方法,用於終止NSTimer。可是,這裏涉及到一個多線程的問題。假設,我在A線程啓動NSTimer,在B線程調用invalidate方法來終止NSTimer,那麼,NSTimer是否會終止呢。

咱們來試驗一下:

dispatch_async(dispatch_get_main_queue(), ^{
        self.timer = [NSTimer scheduledTimerWithTimeInterval:0.5f
                                                      target:self
                                                    selector:@selector(test)
                                                    userInfo:nil
                                                     repeats:YES];
    });
    
dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self.timer invalidate];
    });

結果是並不會中止

NSTimer的官方文檔上,蘋果是這麼說的:

You should always call the invalidate method from the same thread on which the timer was installed.

因此,咱們必須哪一個線程調用,哪一個線程終止。

CFRunLoopTimerRef

NSTimer的官方文檔上,蘋果提到:

NSTimer is 「toll-free bridged」 with its Core Foundation counterpart, CFRunLoopTimerRef. See Toll-Free Bridgingfor more information on toll-free bridging.

NSTimer能夠直接橋接到CFRunLoopTimerRef上,二者的數據結構是相同的,是能夠互換的。咱們能夠理解爲,NSTimer是objc版本的CFRunLoopTimerRef封裝。

用法

CFRunLoopTimerRef通常用法以下:

NSTimeInterval fireDate = CFAbsoluteTimeGetCurrent();
CFRunLoopTimerRef timer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, fireDate, 0.5f, 0, 0, ^(CFRunLoopTimerRef timer) {
        NSLog(@"test");
    });
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopCommonModes);

其餘用法能夠參考蘋果的CFRunLoopTimerRef文檔

Tips

和RunLoop有什麼關係?

CoreFoundation框架中有主要有5個RunLoop類:

  • CFRunLoopRef

  • CFRunLoopModeRef

  • CFRunLoopSourceRef

  • CFRunLoopTimerRef

  • CFRunLoopObserverRef

CFRunLoopTimerRef屬於其中之一。

關於RunLoop,ibireme大神寫了一個很是好的文章,你們能夠圍觀學習一下:

http://blog.ibireme.com/2015/05/18/runlo...

有沒有須要使用CFRunLoopTimerRef?

既然CFRunLoopTimer is 「toll-free bridged」 with its Cocoa Foundation counterpart, NSTimer. ,那麼若是在使用CF框架寫內容的話,能夠直接使用,不然,仍是使用NSTimer吧。

dispatch_after

用法

dispatch_after的用法比較簡單,通常設定一個時間,而後指定一個隊列,好比Main Dispatch Queue

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"test");
    });

和上面說到的NSTimerCFRunLoopTimerRef不一樣,NSTimerCFRunLoopTimerRef是在指定的RunLoop上註冊啓動時間,而dispatch_after是在指定的時間後,將整個執行的Block塊添加到指定隊列的RunLoop上。

因此,若是此隊列處於繁重任務或者阻塞之中,dispatch_after的Block塊確定是要延後執行的。

Tips

強引用

假設如今有這麼一個狀況,dispatch_after中引用了Self,那麼在設定時間以前,Self能夠被釋放嗎?

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.label.text = @"ok";
    });

我來試驗一下,我從A-VC push到B-VC,以後再pop回來,B-VC有個5秒的dispatch_after:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.label.text = @"ok";
        NSLog(@"-- %@",self.description);
    });

咱們看一下輸出:

結果是,在pop回去以後,Self並無獲得釋放,在dispatch_after的Block塊執行完成以後,Self才獲得正確釋放。

如何解決dispatch_after的Block塊中的強引用問題?

MLeaksFinder是一個開源的iOS內存泄露檢測工具。做者頗有想法,他在ViewController被pop或dismiss以後,在必定時間以後(默認爲3秒),去看看這個ViewController和它的subviews是否還存在。

基本上是相似於這樣:

__weak id weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf assertNotDealloc];
    });

這裏若是直接使用Self的話,ViewController被pop或者dismiss以後,依然是沒法釋放的,而__weak就能夠解決這個問題。在這裏,Self若是被正確釋放的話,weakSelf天然會變成nil

咱們修改一下咱們試驗的代碼:

__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        weakSelf.label.text = @"ok";
        NSLog(@"-- %@",weakSelf.description);
    });

OK,沒有問題。

performSelector:withObject:afterDelay:

用法

這依然是對CFRunLoopTimerRef的封裝,和NSTimer同源。既然是同源,那麼一樣具備NSTimer的RunLoop特性。依據蘋果文檔中提到內容,performSelector:withObject:afterDelay:運行的RunLoop模式是NSDefaultRunLoopMode,那麼,ScrollView滾動的時候,RunLoop會切換,performSelector:withObject:afterDelay:天然不會被回調。

This method sets up a timer to perform the aSelector message on the current thread’s run loop. The timer is configured to run in the default mode (NSDefaultRunLoopMode).

performSelector:withObject:afterDelay:的用法也比較簡單。

[self performSelector:@selector(test) withObject:nil afterDelay:1.0f];

Tips

強引用

假設是Self去調用performSelector:withObject:afterDelay:方法,在Delay時間未到以前,Self可否被釋放呢?

咱們試驗一下:

[self performSelector:@selector(printDescription) withObject:nil afterDelay:5.0f];

結果和上面的dispatch_after同樣,咱們修改一下代碼,再看一下:

__weak typeof(self) weakSelf = self;
[weakSelf performSelector:@selector(printDescription) withObject:nil afterDelay:5.0f];

很遺憾,沒有用。可是咱們能夠取消這個performSelector

這種方法能夠取消Target指定的帶執行的方法。

[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(printDescription) object:nil];

這種方法能夠取消Target的全部待執行的方法。

[NSObject cancelPreviousPerformRequestsWithTarget:self];

可否正確釋放返回值?

performSelector不一樣的是,performSelector:withObject:afterDelay:並不會出現」PerformSelector may cause a leak because its selector is unknown.」的警告。

那麼,這是否意味着performSelector:withObject:afterDelay:能夠正確釋放返回值呢?

若是如今performSelector:withObject:afterDelay:所執行的Selector並不肯定,而且可能會返回一個對象,那麼系統可否正確釋放這個返回值呢?

咱們試驗一下,這裏printDescriptionAprintDescriptionB方法各會返回一個不一樣類型的View(此View是新建的對象),printDescriptionC會返回Void

NSArray *array = @[@"printDescriptionA",
                   @"printDescriptionB",
                   @"printDescriptionC"];
        
NSString *selString = array[arc4random()%3];
NSLog(@"sel = %@", selString);
SEL tempSel = NSSelectorFromString(selString);
if ([self respondsToSelector:tempSel])
{
    [self performSelector:tempSel withObject:nil afterDelay:3.0f];
}

幾回嘗試以後,我發現,這是能夠正常釋放的。

Effective Objective-C 2.0一書中,做者在第42條上提到:

performSelector系列方法在內存管理方面容易有疏失。它沒法肯定要執行的選擇子具體是什麼,於是ARC編譯器也就沒法插入適當的內存管理方法。

關於這個問題,stackoverflow上也有不少討論:

http://stackoverflow.com/questions/70172...

我不知道如何觸發這種內存泄露,有知道的告訴我一聲,學習一下,謝謝。

參考資料

  1. https://developer.apple.com/library/ios/...

  2. https://developer.apple.com/library/ios/...

  3. https://developer.apple.com/library/mac/...

  4. https://developer.apple.com/library/ios/...

  5. http://blog.ibireme.com/2015/05/18/runlo...

相關文章
相關標籤/搜索