博文地址:http://ifujun.com/ios-timer-pan-dian/html
在iOS的開發過程當中,Timer是一個很常見的功能。蘋果提供給了咱們好幾種能夠達到Timer效果的方法,我嘗試在這裏盤點一下。ios
NSTimer
是咱們最多見的一種Timer,咱們從NSTimer
開始提及。git
NSTimer
的用法很簡單,我的比較經常使用的是下面這個方法:github
[NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(test) userInfo:nil repeats:nil];
有這麼一道面試題,題目是這樣的:面試
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並非一種實時機制,它只會在下面條件知足的狀況下才會啓動:
NSTimer被添加到的RunLoop模式正在運行。
NSTimer設定的啓動時間尚未過去。
關鍵在於第一點,咱們剛纔的NSTimer
默認添加在NSDefaultRunLoopMode
上,而UIScrollView
在滑動的時候,RunLoop會自動切換到 UITrackingRunLoopMode
,NSTimer
並無添加到這個RunLoop模式上,天然是不會啓動的。
因此,若是咱們想要NSTimer
在UIScrollView
滑動的時候也會啓動的話,只要將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.
因此,咱們必須哪一個線程調用,哪一個線程終止。
在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文檔。
CoreFoundation框架中有主要有5個RunLoop類:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
CFRunLoopTimerRef
屬於其中之一。
關於RunLoop,ibireme大神寫了一個很是好的文章,你們能夠圍觀學習一下:
http://blog.ibireme.com/2015/05/18/runlo...
既然CFRunLoopTimer is 「toll-free bridged」 with its Cocoa Foundation counterpart, NSTimer.
,那麼若是在使用CF框架寫內容的話,能夠直接使用,不然,仍是使用NSTimer
吧。
dispatch_after
的用法比較簡單,通常設定一個時間,而後指定一個隊列,好比Main Dispatch Queue
。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"test"); });
和上面說到的NSTimer
和CFRunLoopTimerRef
不一樣,NSTimer
和CFRunLoopTimerRef
是在指定的RunLoop上註冊啓動時間,而dispatch_after
是在指定的時間後,將整個執行的Block塊添加到指定隊列的RunLoop上。
因此,若是此隊列處於繁重任務或者阻塞之中,dispatch_after
的Block塊確定是要延後執行的。
假設如今有這麼一個狀況,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
才獲得正確釋放。
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,沒有問題。
這依然是對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];
假設是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
並不肯定,而且可能會返回一個對象,那麼系統可否正確釋放這個返回值呢?
咱們試驗一下,這裏printDescriptionA
和printDescriptionB
方法各會返回一個不一樣類型的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...
我不知道如何觸發這種內存泄露,有知道的告訴我一聲,學習一下,謝謝。