NSTimer定时器进阶——详细介绍,循环引用分析与解决
引言
定時器:A timer waits until a certain time interval has elapsed and then fires, sending a specified message to a target object.
翻譯如下:在固定的時間間隔被觸發,然后給指定目標發送消息。總結為三要素吧:時間間隔、被觸發、發送消息(執行方法)
按照官方的描述,我們也確實是這么用的;但是里面有很多細節,你是否了解呢?
- 它會被添加到runloop,否則不會運行,當然添加的runloop不存在也不會運行;
- 還要指定添加到的runloop的哪個模式,而且還可以指定添加到runloop的多個模式,模式不對也是不會運行的
- runloop會對timer有強引用,timer會對目標對象進行強引用(是否隱約的感覺到坑了。。。)
- timer的執行時間并不準確,系統繁忙的話,還會被跳過去
- invalidate調用后,timer停止運行后,就一定能從runloop中消除嗎,資源????
呵呵。。。下面會解決這些問題
定時器的一般用法
控制器中添加定時器,例如:
- (void)viewDidLoad {NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];self.timer = timer; }- (void)timerFire {NSLog(@"timer fire"); }上面的代碼就是我們使用定時器最常用的方式,可以總結為2個步驟:創建,添加到runloop
系統提供了8個創建方法,6個類創建方法,2個實例初始化方法。
- 有三個方法直接將timer添加到了當前runloop default mode,而不需要我們自己操作,當然這樣的代價是runloop只能是當前runloop,模式是default mode:
- 下面五種創建,不會自動添加到runloop,還需調用addTimer:forMode::
對上面所有方法參數做個說明:
添加到runloop,參數timer是不能為空的,否則拋出異常
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;另外,系統提供了一個- (void)fire;方法,調用它可以觸發一次:
- 對于重復定時器,它不會影響正常的定時觸發
- 對于非重復定時器,觸發后就調用了invalidate方法,既使正常的還沒有觸發
NSTimer添加到NSRunLoop
如同引言中說的那樣,timer必須添加到runloop才有效,很明顯要保證兩件事情,一是runloop存在(運行),另一個才是添加。確保這兩個前提后,還有runloop模式的問題。
一個timer可以被添加到runloop的多個模式,比如在主線程中runloop一般處于NSDefaultRunLoopMode,而當滑動屏幕的時候,比如UIScrollView或者它的子類UITableView、UICollectionView等滑動時runloop處于UITrackingRunLoopMode模式下,因此如果你想讓timer在滑動的時候也能夠觸發,就可以分別添加到這兩個模式下。或者直接用NSRunLoopCommonModes一個模式集,包含了上面的兩種模式。
但是一個timer只能添加到一個runloop(runloop與線程一一對應關系,也就是說一個timer只能添加到一個線程)。如果你非要添加到多個runloop,則只有一個有效
關于強引用的問題
還是經常使用到的代碼
- (void)viewDidLoad {// 代碼標記1NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];// 代碼標記2[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];// 代碼標記3self.timer = timer; }- (void)timerFire {NSLog(@"timer fire"); }假設代碼中的視圖控制器由UINavigationController管理,且self.timer是strong類型,則強引用可以表示如下:
上面有四根強引用線,它們是如何產生的呢,這個也必須搞清楚?
- L1:這個簡單,nav push 控制器的時候會強引用,即在push的時候產生;
- L2:是在代碼標記3的位置產生;
- L3:是在代碼標記1的位置產生,至此L2與L3已經產生了循環引用,雖然timer還沒有添加到runloop
- L4:是在代碼標記2的位置產生
根據上圖就很清晰了,我們經常說到timer與self會造成循環引用,并不是因為runloop引起,而是timer本身會對self有強引用。
invalidate方法
invalidate方法有2個功能:一是將timer從runloop中移除,那么圖中的L4就消失,二是timer本身也會釋放它持有資源,比如target、userinfo、block(關于block強引用self具體參考這里:http://www.cnblogs.com/mddblog/p/4754190.html),那么強引用L3就消失。如果self.timer是weak引用,也就是L2是弱引用,那么timer的引用計數就為0了,timer本身也就被釋放了。如果你此時又調用addTimer:forMode:則會拋異常,因為timer為nil,因此當控制器使用weak方式引用timer時,應注意這點
之后的timer也就永遠無效了,調用它的getter方法isValid返回是NO,即使你再次將它正確的添加到runloop,也不會觸發,因為timer已對target、block釋放了。
timer只有這一個方法可以完成此操作,所以我們取消一個timer必須要調用此方法。而在添加到runloop前,可以使用它的getter方法isValid來判斷,一個是防止為nil,另一個是防止為無效。
然而就像引言中說的那個聳人聽聞的問題一樣,invalidate方法調用必須在timer添加到的runloop所在的線程,如果不在的話:雖然timer本身會釋放掉它自己持有的資源比如target、userinfo、block,圖中的L3會消失。但是runloop不會釋放timer,即圖中的L4不會消失,假設,self被pop了-->L1無效-->self引用計數為0,self釋放-->L2也消失。此時就剩runloop、timer、L4,timer也就永遠不會釋放了,造成內存泄露。
下面不得不面對另一個問題,runloop退出或者本身被釋放不就可以了嗎???
這才真心是一個頭疼的問題:是的,沒錯,runloop退出甚至自身釋放后,L4消失,timer也就釋放了。。。可以參考之前那篇關于runloop退出釋放的問題NSRunLoop原理詳解——不再有盲點:http://www.jianshu.com/p/4263188ed940
這里補充一點,timer沒有被釋放,那么它會作為runloop的輸入源,從而阻止runloop的退出(runloop的退出是會釋放掉timer的)。
只關心runloop的退出就好,至于釋放就別深究了,或者就當它不釋放(我的理解是隨著線程釋放而釋放)
關于強引用再舉個常見例子
重復的添加timer,例如下面的代碼:
// 無論self.timer是strong還是weak - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {self.timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:2 target:self selector:@selector(timerHandle) userInfo:nil repeats:YES];[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode]; }每點擊一次屏幕就會添加一次,就會造成重復添加,你的timerHandle方法會被調用多次,添加幾次就調用幾次。。。
假設點擊了2次屏幕,即創建2了個timer,我們標記為t1,t2。我們分析一下:第二次的時候,self.timer引用t2,雖然不在引用t1但是,runloop還在引用它,所以不會釋放,不用說t2也是不會釋放的。
那么如何解決呢?setter方法里面調用invalidate即可:
- (void)setTimer:(NSTimer *)timer {[_timer invalidate];_timer = timer; }其實記住兩條即可
- timer不用了,一定要調用invalidate
- 一般是target釋放的同時,才會知道timer不用了,那么怎么捕獲target被釋放了呢?dealloc方法肯定是不行的。如果是控制器的話可以嘗試監聽pop方法的調用(nav的代理),viewDidDisappear方法里面(但要記著,再次展示的時候從新添加。。。)
不調用invalidate方法,target是不會被釋放的,因為圖中的L4,L3一直存在
timer執行是否準時
不準時!
第一種不準時:有可能跳過去
對于第一種情況我們不應該在timer上下功夫,而是應該避免這個耗時的工作。那么第二種情況,作為開發者這也是最應該去關注的地方,要留意,然后視情況而定是否將timer添加到runloop多個模式
雖然跳過去,但是,接下來的執行不會依據被延遲的時間加上間隔時間,而是根據之前的時間來執行。比如:
定時時間間隔為2秒,t1秒添加成功,那么會在t2、t4、t6、t8、t10秒注冊好事件,并在這些時間觸發。假設第3秒時,執行了一個超時操作耗費了5.5秒,則觸發時間是:t2、t8.5、t10,第4和第6秒就被跳過去了,雖然在t8.5秒觸發了一次,但是下一次觸發時間是t10,而不是t10.5。
第二種不準時:不準點
比如上面說的t2、t4、t6、t8、t10,并不會在準確的時間觸發,而是會延遲個很小的時間,原因也可以歸結為2點:
以我來講,從來沒有特別準的時間,
iOS7以后,Timer 有個屬性叫做 Tolerance (時間寬容度,默認是0),標示了當時間點到后,容許有多少最大誤差。
它只會在準確的觸發時間到加上Tolerance時間內觸發,而不會提前觸發(是不是有點像我們的火車,只會晚點。。。)。另外可重復定時器的觸發時間點不受Tolerance影響,即類似上面說的t8.5觸發后,下一個點不會是t10.5,而是t10 + Tolerance,不讓timer因為Tolerance而產生漂移(突然想起嵌入式令人頭疼的溫漂)。
其實對于這種不準點,對我們開發影響并不大(基本是毫秒妙級別以下的延遲),很少會用到非常準點的情況。
GCD定時器簡單介紹
其實這種我們平時也經常用(一次性定時):
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);when接受兩種類型參數:dispatch_time相對時間,相對系統的時間,比如上面相對于DISPATCH_TIME_NOW;dispatch_walltime是絕對時間,比如某年月日某時分秒。。。之后由GCD幫我們計算一個相對時間。下面說下dispatch_time,支持納秒級別
dispatch_time_t when = dispatch_time (DISPATCH_TIME_NOW, 1);// 還沒這么用過1納秒的延遲應該很準確了,但是定時時間到后只是將block添加到指定的queue,去執行。這樣的話,執行時間也是不保證的,首先執行線程要等待內核的調度,其次執行線程正好沒有其它事情做。如果還需要創建線程的話,就更浪費時間了。所以這個也是不符合我們期望的
when也支持DISPATCH_TIME_NOW,但是這樣就沒意義了,不如直接調用dispatch_async。而至于DISPATCH_TIME_FOREVER就更。。。
重復性定時,代碼示例如下:
// 需要強引用 @property (nonatomic, strong)dispatch_source_t gcdTime;- (void)gcdTimerTest {// 這里需要強引用self.gcdTime = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));// 開始時間支持納秒級別dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)2 * NSEC_PER_SEC);// 2秒執行一次uint64_t dur = (uint64_t)(2.0 * NSEC_PER_SEC);// 最后一個參數是允許的誤差,即使設為零,系統也會有默認的誤差dispatch_source_set_timer(self.gcdTime, start, dur, 0);// 設置回調dispatch_source_set_event_handler(self.gcdTime, ^{NSLog(@"---%@---%@",[NSThread currentThread],self);});dispatch_resume(self.gcdTime); }取消定時器:dispatch_cancel(self.gcdTimer);,取消后再次調用dispatch_source_set_timer是沒有用的。self.gcdTimer已不可用
雖然支持納秒級別,但是定時也是不準的,上面的例子使用的是dispatch_get_global_queue隊列,執行線程也是不確定的。所以在實際開發中這種很少用,好處是它不受runloop mode限制
轉載于:https://www.cnblogs.com/mddblog/p/6517377.html
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的NSTimer定时器进阶——详细介绍,循环引用分析与解决的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android studio 运行即打包
- 下一篇: 35:字符串的展开