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