重新認識了NSTimer以及他與RunLoop關系

已經將近兩年沒有寫過文章了,之前記錄的知識點都在有道筆記上,看到網上那么多人分享知識,突然也想重新寫了,分享知識能夠使自己學到更多.
最近在查閱iOS中RunLoop資料時無意間看到了NSTimer與RunLoop的關系,于是開始去了解NSTimer,發現之前對NSTimer的運用只是把代碼寫上了,并沒有深入去了解他里面存在的問題.通過查看資料,以及自己寫代碼測試,現將學到的知識總結一下,里面有認識理解不正確的歡迎指正

一.NSTimer創建方法
我個人認為NSTimer的創建可以分為三種
1.scheduledTimer創建

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

2.timerWithTimeInterval類方法

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

3.init創建

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

那么他們有什么區別呢?
大部分人習慣使用方法1,簡單直接,有的人習慣性的設置一個全局變量,在viewWillDisappear:或者viewDidDisappear:方法中寫上

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

并沒有想過為什么,而大多數情況,我們設置Timer也是在主線程中,也并未出現過Timer設置完后無效的情況,所以都沒有去深入研究過.
接下來我們一個問題一個問題的說:
其實NSTimer和Runloop有著密不可分的關系(這里不是講Runloop的,而我也并沒有對runloop了解特別深入,所以不多說),大部分人直接使用scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:方法創建Timer,只要創建好,就可以直接執行Timer的觸發事件,因為這個方法系統會默認為我們添加到Runloop的NSDefaultRunLoopMode中,通過代碼用各種方法創建Timer測試

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

點擊按鈕,我們會發現控制臺輸出如下

1.png

接下來用另外兩種方法創建,以上代碼不再重復,直接寫case: 內容

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

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

當我們把viewDidLoad方法中調用的initTestTimerWithMethod: repeats:方法參數改為1時,點擊按鈕,會發現控制臺沒有任何輸出,將參數改為2同樣控制臺沒有任何輸出,查閱官方文檔發現,原來這兩種方法創建的Timer,不會自動添加到Runloop中,需要我們手動添加到當前的Runloop中才會執行,也就是說明Timer與Runloop有著密不可分的關系,于是修改代碼

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

修改后把viewDidLoad方法中調用的initTestTimerWithMethod: repeats:方法參數分別改為1和2依次運行代碼,會發現控制臺有輸出

每個線程都對應一個Runloop,而主線程的Runloop默認是開啟的,子線程的Runloop默認不是開啟的.通常情況我們的Timer是在主線程中創建的,但是也不乏有的時候是在子線程中創建的,前段時間我就遇到了問題,我們公司是做軟硬件的,產品智能音箱需要聯網,其中一種聯網方法是熱點聯網,用到了TCP,UDP,通常我們是要另開線程創建Socket的,Socket連接以及數據發送等在任何一個過程中都有可能失敗,這里不詳細說明,我們的需求是在建立TCP,UDP連接,發送數據以及聯網過程加入定時器設置總超時時間,測試測出bug,在一個特定條件下,APP界面的聯網提示一直不消失,當時花了很長時間解決這個bug,因為聯網過程分了很多步,在任何一步都可能失敗,最終發現只要在那個特定的一步出錯導致聯網失敗都會出現這個bug,找了很久才發現是因為Timer設置的執行方法沒有執行,但是也想不明白為什么沒有執行,在網上查閱資料才知道,原來在子線程創建Timer需要加入Runloop中并開啟Runloop,不妨測試一下

case3:
{
[self createThread];
}
break;

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

我們先把[[NSRunLoopcurrentRunLoop]run];這行代碼注釋掉,會發現控制臺沒有任何輸出,但是添加上這行代碼,Timer的執行事件會正常觸發.所以要注意在子線程中創建Timer,一定要開始當前線程的Runloop.
二,Timer正常執行后也會遇到的問題
1.循環引用,內存泄露
前面我們已經提到,通常我們會將Timer設置為全局變量,在界面將要消失或者消失的時候將Timer invalidate掉,這是為什么呢?下面我們就來探討一下
其實Timer會強引用自己的target對象的,而target對象也會對Timer強引用,不妨我們測試一下,還是上面的代碼,我們在dealloc方法中打印

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

這里補充一下,當前TestTimerViewController是由ViewController presen過來的第二個VC,點擊關閉按鈕,返回ViewController,presen進來的時候Timer觸發.這時候會發現控制臺輸出了,點擊關閉按鈕返回主界面,dealloc方法并沒有調用,而且無論是調用哪種方法創建的Timer都是沒有調用dealloc方法,認真觀察我們發現,以上調用的Timer都是重復執行的,即repeats的值為YES,那我們改為NO結果會怎么樣呢?

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

我們看控制臺輸出,結果是無論調用哪種方法,返回上一界面的時候都會發現調用dealloc方法了,但是重復執行的時候dealloc始終沒有調用,這個時候怎么辦呢?
我們只需要在界面消失的時候將Timer invalidate

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

這時候再將repeates值修改為YES會看到返回界面的時候控制臺輸出了dealloc,即調用了dealloc方法, 但是還有一種情況,我們這里是TestTimerViewController強引用了_testTimer,那如果只是單單的創建一個臨時變量的Timer的時候上面的現象還會發生嗎? 不妨試一試

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

我們會看到答案是YES,當repeats:參數為YES的時候,返回時dealloc仍然不會調用,當repeats參數為NO時候,返回上一界面dealloc會調用
總結: 我們在使用Timer的時候,只要創建了Timer,持有Timer的對象都會對Timer強引用,而Timer的target對象也會被Timer強引用,其實根本原因是Timer在isValid為YES的時候是強引用自己的target的對象,當界面回收的時候Timer持有VC,回收Timer時候要回收發現VC持有Timer,這樣就造成循環引用. 但是當Timer的target觸發事件是只有一次即repeats參數為NO時候,Timer會invalidate自身,這樣VC也會回收,當Timer的target觸發事件是重復的即repeats參數為YES的時候,Timer不會invalidate自身,需要我們自己手動invalidate,所以在使用NSTimer的時候最好用全局變量定義,界面消失的時候要將Timer invalidate掉,這樣才會避免由于循環引用造成的內存泄露

2,Timer中Runloop的mode
我們有時在使用Timer的時候會發現他觸發事件的時機不對,這就與Runloop相關了,一個RunLoop包含若干個Mode,每個Mode又包含若干個Source/Timer/Observer.每次調用RunLoop的主函數時,只能指定其中一個Mode,這個Mode被稱為CurrentMode,Runloop的模式也分為幾種:常見的是default和common modes模式以及event tracking模式(組件拖動輸入源 UITrackingRunLoopModes 不處理定時事件),而connection模式(處理NSConnection事件,屬于系統內部)用戶基本不用.這里需要強調common modes模式:NSRunLoopCommonModes 這是一組可配置的通用模式。將input sources與該模式關聯則同時也將input sources與該組中的其它模式進行了關聯.每次運行一個run loop,你指定run loop的運行模式。當相應的模式傳遞給run loop時,只有與該模式對應的 input sources才被監控并允許run loop對事件進行處理(與此類似,也只有與該模式對應的observers才會被通知),針對不同的Mode系統有不同的處理策略和優先級,而default Mode是優先級比較低的,例如當我們在滑動屏幕的時候,其Runloop的mode會切換到event tracking模式,event tracking模式是不處理定時事件的,所以此時當我們的Timer添加的Runloop的模式是default的時候,Timer的事件是不執行的,只有滑動結束了,又重新切換到default模式時候Timer才會執行,而此時他會把之前這段時間的Timer的事件都一次性執行,因為為了避免這種情況發生,我們通常把他添加到Runloop中,設置模式為common modes.話不多說,看代碼

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

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

這里我創建的TestTimerViewController直接繼承自UITableViewController,我設置了100行數據,可以自由滑動,將Timer添加的Runloop的mode設置為NSRunLoopCommonModes,滑動過程看控制臺輸出情況會發現Timer的觸發事件仍然是每隔兩秒執行一次

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

我們會發現事件觸發時間與我們設置的不同,
同時你會發現在子線程創建的Timer默認添加到當前的的Runloop,其mode是default,但是當我們滑動屏幕的時候,并不會影響Timer的執行時間,因為他是在子線程中的Runloop中,而滑動事件是在主線程中的,這里就不再上代碼了
三,GCD定時
相信用GCD定時器的人不太多,我也是之前在一個demo上看到這些代碼后,才去搜索查看的,GCD定時不需要我們的管理內存釋放,我們只需要寫出想要執行的事件.
1.只執行一次

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

2.重復執行

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

總結
1.使用Timer的時候最好使用全局變量,在頁面消失的時候將Timer invalidate掉,防止循環引用造成的內存泄露(當然了,是在repeats值為YES的時候)
2.子線程中創建Timer要將其Runloop開啟[[NSRunLoopcurrentRunLoop]run];否則會不執行Timer事件
3.最好將Timer添加到Runloop的Mode設置為CommonModes

最后,對于Runloop,我還了解的不夠好,希望再多查資料,多運用,大家也可以多研究研究,上面有不對的地方還請提出寶貴意見

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容