重新認(rèn)識(shí)了NSTimer以及他與RunLoop關(guān)系

已經(jīng)將近兩年沒(méi)有寫(xiě)過(guò)文章了,之前記錄的知識(shí)點(diǎn)都在有道筆記上,看到網(wǎng)上那么多人分享知識(shí),突然也想重新寫(xiě)了,分享知識(shí)能夠使自己學(xué)到更多.
最近在查閱iOS中RunLoop資料時(shí)無(wú)意間看到了NSTimer與RunLoop的關(guān)系,于是開(kāi)始去了解NSTimer,發(fā)現(xiàn)之前對(duì)NSTimer的運(yùn)用只是把代碼寫(xiě)上了,并沒(méi)有深入去了解他里面存在的問(wèn)題.通過(guò)查看資料,以及自己寫(xiě)代碼測(cè)試,現(xiàn)將學(xué)到的知識(shí)總結(jié)一下,里面有認(rèn)識(shí)理解不正確的歡迎指正

一.NSTimer創(chuàng)建方法
我個(gè)人認(rèn)為NSTimer的創(chuàng)建可以分為三種
1.scheduledTimer創(chuàng)建

1),scheduledTimerWithTimeInterval: invocation: repeats:
2),scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:

2.timerWithTimeInterval類(lèi)方法

1),timerWithTimeInterval: target: selector: userInfo: repeats:
2),timerWithTimeInterval: invocation: repeats:

3.init創(chuàng)建

initWithFireDate: interval: target: selector: userInfo: repeats:

那么他們有什么區(qū)別呢?
大部分人習(xí)慣使用方法1,簡(jiǎn)單直接,有的人習(xí)慣性的設(shè)置一個(gè)全局變量,在viewWillDisappear:或者viewDidDisappear:方法中寫(xiě)上

if(self.testTimer.isValid) {
[self.testTimerinvalidate];
}
self.testTimer=nil;

并沒(méi)有想過(guò)為什么,而大多數(shù)情況,我們?cè)O(shè)置Timer也是在主線(xiàn)程中,也并未出現(xiàn)過(guò)Timer設(shè)置完后無(wú)效的情況,所以都沒(méi)有去深入研究過(guò).
接下來(lái)我們一個(gè)問(wèn)題一個(gè)問(wèn)題的說(shuō):
其實(shí)NSTimer和Runloop有著密不可分的關(guān)系(這里不是講Runloop的,而我也并沒(méi)有對(duì)runloop了解特別深入,所以不多說(shuō)),大部分人直接使用scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:方法創(chuàng)建Timer,只要?jiǎng)?chuàng)建好,就可以直接執(zhí)行Timer的觸發(fā)事件,因?yàn)檫@個(gè)方法系統(tǒng)會(huì)默認(rèn)為我們添加到Runloop的NSDefaultRunLoopMode中,通過(guò)代碼用各種方法創(chuàng)建Timer測(cè)試

- (void)viewDidLoad {
[superviewDidLoad];
self.view.backgroundColor= [UIColorwhiteColor];
[selfcreateView];
[self initTestTimerWithMethod:0 repeats:YES];
//[self createCustomTimer];
//[self createThread];
}
#pragma mark - NSTimer
//創(chuàng)建NSTimer
- (void)initTestTimerWithMethod:(int)method repeats:(BOOL)repeat {
switch(method) {
case0://scheduledTimerWithTimeInterval:方法創(chuàng)建
{
//會(huì)自動(dòng)執(zhí)行,并且自動(dòng)加入當(dāng)前線(xiàn)程的Run Loop中其mode為:NSDefaultRunLoopMode
self.testTimer= [NSTimerscheduledTimerWithTimeInterval:2target:selfselector:@selector(timerAction1)userInfo:nilrepeats:repeat];
}
break;
default:
break;
}
}
- (void)timerAction1 {
NSLog(@"scheduledTimer方法%@",@"執(zhí)行Timer事件");
}

點(diǎn)擊按鈕,我們會(huì)發(fā)現(xiàn)控制臺(tái)輸出如下

1.png

接下來(lái)用另外兩種方法創(chuàng)建,以上代碼不再重復(fù),直接寫(xiě)case: 內(nèi)容

case1://timerWithTimeInterval:方法創(chuàng)建
{
//需要手動(dòng)加入主循環(huán)池中
self.testTimer= [NSTimertimerWithTimeInterval:2target:selfselector:@selector(timerAction2)userInfo:nilrepeats:repeat];
}
break;
case2://initWithFireDate:方法創(chuàng)建
{
//init方法需要手動(dòng)加入循環(huán)池,它會(huì)在設(shè)定的啟動(dòng)時(shí)間啟動(dòng)
self.testTimer= [[NSTimeralloc]initWithFireDate:[NSDatedateWithTimeIntervalSinceNow:5]interval:1target:selfselector:@selector(timerAction3)userInfo:nilrepeats:repeat];
}
break;

//Timer執(zhí)行方法
- (void)timerAction2 {
NSLog(@"timerWithTimeInterval:方法%@",@"執(zhí)行Timer事件");
}
- (void)timerAction3 {
NSLog(@"initWithFireDate:方法%@",@"執(zhí)行Timer事件");
}

當(dāng)我們把viewDidLoad方法中調(diào)用的initTestTimerWithMethod: repeats:方法參數(shù)改為1時(shí),點(diǎn)擊按鈕,會(huì)發(fā)現(xiàn)控制臺(tái)沒(méi)有任何輸出,將參數(shù)改為2同樣控制臺(tái)沒(méi)有任何輸出,查閱官方文檔發(fā)現(xiàn),原來(lái)這兩種方法創(chuàng)建的Timer,不會(huì)自動(dòng)添加到Runloop中,需要我們手動(dòng)添加到當(dāng)前的Runloop中才會(huì)執(zhí)行,也就是說(shuō)明Timer與Runloop有著密不可分的關(guān)系,于是修改代碼

case1://timerWithTimeInterval:方法創(chuàng)建
{
//需要手動(dòng)加入主循環(huán)池中
self.testTimer= [NSTimertimerWithTimeInterval:2target:selfselector:@selector(timerAction2)userInfo:nilrepeats:repeat];
[[NSRunLoop currentRunLoop]addTimer:self.testTimerforMode:NSDefaultRunLoopMode];
}
break;
case2://initWithFireDate:方法創(chuàng)建
{
//init方法需要手動(dòng)加入循環(huán)池,它會(huì)在設(shè)定的啟動(dòng)時(shí)間啟動(dòng)
self.testTimer= [[NSTimeralloc]initWithFireDate:[NSDatedateWithTimeIntervalSinceNow:5]interval:1target:selfselector:@selector(timerAction3)userInfo:nilrepeats:repeat];
[[NSRunLoopcurrentRunLoop]addTimer:self.testTimerforMode:NSDefaultRunLoopMode];
}
break;

修改后把viewDidLoad方法中調(diào)用的initTestTimerWithMethod: repeats:方法參數(shù)分別改為1和2依次運(yùn)行代碼,會(huì)發(fā)現(xiàn)控制臺(tái)有輸出

每個(gè)線(xiàn)程都對(duì)應(yīng)一個(gè)Runloop,而主線(xiàn)程的Runloop默認(rèn)是開(kāi)啟的,子線(xiàn)程的Runloop默認(rèn)不是開(kāi)啟的.通常情況我們的Timer是在主線(xiàn)程中創(chuàng)建的,但是也不乏有的時(shí)候是在子線(xiàn)程中創(chuàng)建的,前段時(shí)間我就遇到了問(wèn)題,我們公司是做軟硬件的,產(chǎn)品智能音箱需要聯(lián)網(wǎng),其中一種聯(lián)網(wǎng)方法是熱點(diǎn)聯(lián)網(wǎng),用到了TCP,UDP,通常我們是要另開(kāi)線(xiàn)程創(chuàng)建Socket的,Socket連接以及數(shù)據(jù)發(fā)送等在任何一個(gè)過(guò)程中都有可能失敗,這里不詳細(xì)說(shuō)明,我們的需求是在建立TCP,UDP連接,發(fā)送數(shù)據(jù)以及聯(lián)網(wǎng)過(guò)程加入定時(shí)器設(shè)置總超時(shí)時(shí)間,測(cè)試測(cè)出bug,在一個(gè)特定條件下,APP界面的聯(lián)網(wǎng)提示一直不消失,當(dāng)時(shí)花了很長(zhǎng)時(shí)間解決這個(gè)bug,因?yàn)槁?lián)網(wǎng)過(guò)程分了很多步,在任何一步都可能失敗,最終發(fā)現(xiàn)只要在那個(gè)特定的一步出錯(cuò)導(dǎo)致聯(lián)網(wǎng)失敗都會(huì)出現(xiàn)這個(gè)bug,找了很久才發(fā)現(xiàn)是因?yàn)門(mén)imer設(shè)置的執(zhí)行方法沒(méi)有執(zhí)行,但是也想不明白為什么沒(méi)有執(zhí)行,在網(wǎng)上查閱資料才知道,原來(lái)在子線(xiàn)程創(chuàng)建Timer需要加入Runloop中并開(kāi)啟Runloop,不妨測(cè)試一下

case3:
{
[self createThread];
}
break;

//多線(xiàn)程創(chuàng)建Timer
- (void)createThread {
//NSLog(@"主線(xiàn)程%@", [NSThread currentThread]);
//創(chuàng)建并執(zhí)行新的線(xiàn)程
NSThread*thread = [[NSThreadalloc]initWithTarget:selfselector:@selector(createTimerWithThread)object:nil];
[threadstart];
}
- (void)createTimerWithThread {
//在當(dāng)前Run Loop中添加timer,模式是默認(rèn)的NSDefaultRunLoopMode
self.threadTimer= [NSTimerscheduledTimerWithTimeInterval:2.0target:selfselector:@selector(threadTimerAction)userInfo:nilrepeats:YES];
//開(kāi)始執(zhí)行子線(xiàn)程的Run Loop
[[NSRunLoopcurrentRunLoop]run];
}
//子線(xiàn)程中timer的回調(diào)方法
- (void)threadTimerAction {
NSLog(@"子線(xiàn)程中創(chuàng)建Timer %@",@"執(zhí)行Timer事件");
}

我們先把[[NSRunLoopcurrentRunLoop]run];這行代碼注釋掉,會(huì)發(fā)現(xiàn)控制臺(tái)沒(méi)有任何輸出,但是添加上這行代碼,Timer的執(zhí)行事件會(huì)正常觸發(fā).所以要注意在子線(xiàn)程中創(chuàng)建Timer,一定要開(kāi)始當(dāng)前線(xiàn)程的Runloop.
二,Timer正常執(zhí)行后也會(huì)遇到的問(wèn)題
1.循環(huán)引用,內(nèi)存泄露
前面我們已經(jīng)提到,通常我們會(huì)將Timer設(shè)置為全局變量,在界面將要消失或者消失的時(shí)候?qū)imer invalidate掉,這是為什么呢?下面我們就來(lái)探討一下
其實(shí)Timer會(huì)強(qiáng)引用自己的target對(duì)象的,而target對(duì)象也會(huì)對(duì)Timer強(qiáng)引用,不妨我們測(cè)試一下,還是上面的代碼,我們?cè)?code>dealloc方法中打印

- (void)dealloc {
NSLog(@"dealloc");
}

這里補(bǔ)充一下,當(dāng)前TestTimerViewController是由ViewController presen過(guò)來(lái)的第二個(gè)VC,點(diǎn)擊關(guān)閉按鈕,返回ViewController,presen進(jìn)來(lái)的時(shí)候Timer觸發(fā).這時(shí)候會(huì)發(fā)現(xiàn)控制臺(tái)輸出了,點(diǎn)擊關(guān)閉按鈕返回主界面,dealloc方法并沒(méi)有調(diào)用,而且無(wú)論是調(diào)用哪種方法創(chuàng)建的Timer都是沒(méi)有調(diào)用dealloc方法,認(rèn)真觀(guān)察我們發(fā)現(xiàn),以上調(diào)用的Timer都是重復(fù)執(zhí)行的,即repeats的值為YES,那我們改為NO結(jié)果會(huì)怎么樣呢?

- (void)viewDidLoad {
[superviewDidLoad];
self.view.backgroundColor= [UIColorwhiteColor];
[selfcreateView];
[selfinitTestTimerWithMethod:2repeats:NO];
}

我們看控制臺(tái)輸出,結(jié)果是無(wú)論調(diào)用哪種方法,返回上一界面的時(shí)候都會(huì)發(fā)現(xiàn)調(diào)用dealloc方法了,但是重復(fù)執(zhí)行的時(shí)候dealloc始終沒(méi)有調(diào)用,這個(gè)時(shí)候怎么辦呢?
我們只需要在界面消失的時(shí)候?qū)imer invalidate

- (void)viewWillDisappear:(BOOL)animated {
[superviewWillDisappear:animated];
//在invalidate之前最好先用isValid先判斷是否還在線(xiàn)程中
//將定時(shí)器從循環(huán)池中移除。
if(self.testTimer.isValid) {
[self.testTimerinvalidate];
}
self.testTimer=nil;
}

這時(shí)候再將repeates值修改為YES會(huì)看到返回界面的時(shí)候控制臺(tái)輸出了dealloc,即調(diào)用了dealloc方法, 但是還有一種情況,我們這里是TestTimerViewController強(qiáng)引用了_testTimer,那如果只是單單的創(chuàng)建一個(gè)臨時(shí)變量的Timer的時(shí)候上面的現(xiàn)象還會(huì)發(fā)生嗎? 不妨試一試

- (void)viewDidLoad {
[superviewDidLoad];
self.view.backgroundColor= [UIColorwhiteColor];
[selfcreateView];
[selfcreateCustomTimer];
}
//無(wú)全局變量創(chuàng)建Timer
- (void)createCustomTimer {
[NSTimerscheduledTimerWithTimeInterval:1target:selfselector:@selector(customTimerAction)userInfo:nilrepeats:YES];
}

我們會(huì)看到答案是YES,當(dāng)repeats:參數(shù)為YES的時(shí)候,返回時(shí)dealloc仍然不會(huì)調(diào)用,當(dāng)repeats參數(shù)為NO時(shí)候,返回上一界面dealloc會(huì)調(diào)用
總結(jié): 我們?cè)谑褂肨imer的時(shí)候,只要?jiǎng)?chuàng)建了Timer,持有Timer的對(duì)象都會(huì)對(duì)Timer強(qiáng)引用,而Timer的target對(duì)象也會(huì)被Timer強(qiáng)引用,其實(shí)根本原因是Timer在isValid為YES的時(shí)候是強(qiáng)引用自己的target的對(duì)象,當(dāng)界面回收的時(shí)候Timer持有VC,回收Timer時(shí)候要回收發(fā)現(xiàn)VC持有Timer,這樣就造成循環(huán)引用. 但是當(dāng)Timer的target觸發(fā)事件是只有一次即repeats參數(shù)為NO時(shí)候,Timer會(huì)invalidate自身,這樣VC也會(huì)回收,當(dāng)Timer的target觸發(fā)事件是重復(fù)的即repeats參數(shù)為YES的時(shí)候,Timer不會(huì)invalidate自身,需要我們自己手動(dòng)invalidate,所以在使用NSTimer的時(shí)候最好用全局變量定義,界面消失的時(shí)候要將Timer invalidate掉,這樣才會(huì)避免由于循環(huán)引用造成的內(nèi)存泄露

2,Timer中Runloop的mode
我們有時(shí)在使用Timer的時(shí)候會(huì)發(fā)現(xiàn)他觸發(fā)事件的時(shí)機(jī)不對(duì),這就與Runloop相關(guān)了,一個(gè)RunLoop包含若干個(gè)Mode,每個(gè)Mode又包含若干個(gè)Source/Timer/Observer.每次調(diào)用RunLoop的主函數(shù)時(shí),只能指定其中一個(gè)Mode,這個(gè)Mode被稱(chēng)為CurrentMode,Runloop的模式也分為幾種:常見(jiàn)的是default和common modes模式以及event tracking模式(組件拖動(dòng)輸入源 UITrackingRunLoopModes 不處理定時(shí)事件),而connection模式(處理NSConnection事件,屬于系統(tǒng)內(nèi)部)用戶(hù)基本不用.這里需要強(qiáng)調(diào)common modes模式:NSRunLoopCommonModes 這是一組可配置的通用模式。將input sources與該模式關(guān)聯(lián)則同時(shí)也將input sources與該組中的其它模式進(jìn)行了關(guān)聯(lián).每次運(yùn)行一個(gè)run loop,你指定run loop的運(yùn)行模式。當(dāng)相應(yīng)的模式傳遞給run loop時(shí),只有與該模式對(duì)應(yīng)的 input sources才被監(jiān)控并允許run loop對(duì)事件進(jìn)行處理(與此類(lèi)似,也只有與該模式對(duì)應(yīng)的observers才會(huì)被通知),針對(duì)不同的Mode系統(tǒng)有不同的處理策略和優(yōu)先級(jí),而default Mode是優(yōu)先級(jí)比較低的,例如當(dāng)我們?cè)诨瑒?dòng)屏幕的時(shí)候,其Runloop的mode會(huì)切換到event tracking模式,event tracking模式是不處理定時(shí)事件的,所以此時(shí)當(dāng)我們的Timer添加的Runloop的模式是default的時(shí)候,Timer的事件是不執(zhí)行的,只有滑動(dòng)結(jié)束了,又重新切換到default模式時(shí)候Timer才會(huì)執(zhí)行,而此時(shí)他會(huì)把之前這段時(shí)間的Timer的事件都一次性執(zhí)行,因?yàn)闉榱吮苊膺@種情況發(fā)生,我們通常把他添加到Runloop中,設(shè)置模式為common modes.話(huà)不多說(shuō),看代碼

case0://scheduledTimerWithTimeInterval:方法創(chuàng)建
{
//會(huì)自動(dòng)執(zhí)行,并且自動(dòng)加入當(dāng)前線(xiàn)程的Run Loop中其mode為:NSDefaultRunLoopMode
self.testTimer= [NSTimerscheduledTimerWithTimeInterval:2target:selfselector:@selector(timerAction1)userInfo:nilrepeats:repeat];
[[NSRunLoopcurrentRunLoop]addTimer:self.testTimerforMode:NSRunLoopCommonModes];
}
break;

//Timer執(zhí)行方法
- (void)timerAction1 {
NSLog(@"scheduledTimer方法%@",@"執(zhí)行Timer事件");
NSLog(@"timerAction1 %@", [[NSRunLoopcurrentRunLoop]currentMode]);
}
- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
NSLog(@"滑動(dòng)屏幕時(shí)%@", [[NSRunLoopcurrentRunLoop]currentMode]);
}

這里我創(chuàng)建的TestTimerViewController直接繼承自UITableViewController,我設(shè)置了100行數(shù)據(jù),可以自由滑動(dòng),將Timer添加的Runloop的mode設(shè)置為NSRunLoopCommonModes,滑動(dòng)過(guò)程看控制臺(tái)輸出情況會(huì)發(fā)現(xiàn)Timer的觸發(fā)事件仍然是每隔兩秒執(zhí)行一次

但是若將其模式更改為defaultMode,則控制臺(tái)輸出如下,
[圖片上傳中。。。(5)]

我們會(huì)發(fā)現(xiàn)事件觸發(fā)時(shí)間與我們?cè)O(shè)置的不同,
同時(shí)你會(huì)發(fā)現(xiàn)在子線(xiàn)程創(chuàng)建的Timer默認(rèn)添加到當(dāng)前的的Runloop,其mode是default,但是當(dāng)我們滑動(dòng)屏幕的時(shí)候,并不會(huì)影響Timer的執(zhí)行時(shí)間,因?yàn)樗窃谧泳€(xiàn)程中的Runloop中,而滑動(dòng)事件是在主線(xiàn)程中的,這里就不再上代碼了
三,GCD定時(shí)
相信用GCD定時(shí)器的人不太多,我也是之前在一個(gè)demo上看到這些代碼后,才去搜索查看的,GCD定時(shí)不需要我們的管理內(nèi)存釋放,我們只需要寫(xiě)出想要執(zhí)行的事件.
1.只執(zhí)行一次

- (void)createGCDTimerSourceActionOnce {
delayInSeconds=2.0;
//參數(shù)1:開(kāi)始執(zhí)行的時(shí)間,參數(shù)2:延遲時(shí)間(單位是納秒)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,delayInSeconds*NSEC_PER_SEC),dispatch_get_main_queue(), ^(void){
//執(zhí)行事件
NSLog(@"GCD定時(shí)器只執(zhí)行一次");
});
}

2.重復(fù)執(zhí)行

- (void)createGCDTimerSourceActionRepeat {
delayInSeconds=2.0;
//創(chuàng)建Dispatch Source
GCDTimerSource=dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,0,0,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0));
//設(shè)置Timer的參數(shù)
//參數(shù)1: dispatch_source_t,參數(shù)2:開(kāi)始執(zhí)行的時(shí)間,參數(shù)3:執(zhí)行時(shí)間間隔(單位是納秒),參數(shù)4:時(shí)間精度(系統(tǒng)可以延時(shí)的時(shí)間間隔)
//系統(tǒng)已預(yù)訂了宏NSEC_PER_SEC,設(shè)置時(shí)間:間隔時(shí)間(單位秒)*NSEC_PER_SEC
dispatch_source_set_timer(GCDTimerSource,DISPATCH_TIME_NOW,delayInSeconds*NSEC_PER_SEC,0.0);
//設(shè)置Dispatch Source的事件回調(diào)
dispatch_source_set_event_handler(GCDTimerSource, ^{
//重復(fù)執(zhí)行的事件
NSLog(@"GCD定時(shí)器重復(fù)執(zhí)行");
});
//dispatch_source默認(rèn)是掛起的狀態(tài),通過(guò)dispatch_resume函數(shù)開(kāi)啟
dispatch_resume(GCDTimerSource);
}

總結(jié)
1.使用Timer的時(shí)候最好使用全局變量,在頁(yè)面消失的時(shí)候?qū)imer invalidate掉,防止循環(huán)引用造成的內(nèi)存泄露(當(dāng)然了,是在repeats值為YES的時(shí)候)
2.子線(xiàn)程中創(chuàng)建Timer要將其Runloop開(kāi)啟[[NSRunLoopcurrentRunLoop]run];否則會(huì)不執(zhí)行Timer事件
3.最好將Timer添加到Runloop的Mode設(shè)置為CommonModes

最后,對(duì)于Runloop,我還了解的不夠好,希望再多查資料,多運(yùn)用,大家也可以多研究研究,上面有不對(duì)的地方還請(qǐng)?zhí)岢鰧氋F意見(jiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容