【iOS開發每日小筆記(九)】在子線程中使用runloop,正確操作NSTimer計時的注意點 三種可選方法

這篇文章是我的【iOS開發每日小筆記】系列中的一片,記錄的是今天在開發工作中遇到的,可以用很短的文章或很小的 demo演示解釋出來的小心得小技巧。它們可能會給用戶體驗、代碼效率得到一些提升,或是之前自己沒有接觸過的技術,很開心的學到了,放在這裏得瑟一下。 其實,90%的作用是幫助自己回顧、記憶、複習。

 

一直想寫一篇關於runloop學習有所得的文章,總是沒有很好的例子。正巧自己的上線App Store的小遊戲《跑酷好基友》(中有一個很好的實際使用例子。遊戲中有一個計時功能。在1.0版本中,使用了簡單的在主線程中調用:

1 + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

的方法。但是當每0.01秒進行一次repeat操作時,NSTimer是不準的,嚴重滯後,而改成0.1秒repeat操作,則這種滯後要好一些。

導致誤差的原因是我在使用“scheduledTimerWithTimeInterval”方法時,NSTimer實例是被加到當前 runloop中的,模式是NSDefaultRunLoopMode。而“當前runloop”就是應用程序的main runloop,此main runloop負責了所有的主線程事件,這其中包括了UI界面的各種事件。當主線程中進行復雜的運算,或者進行UI界面操作時,由於在main runloop中NSTimer是同步交付的被“阻塞”,而模式也有可能會改變。因此,就會導致NSTimer計時出現延誤。

解決這種誤差的方法,一種是在子線程中進行NSTimer的操作,再在主線程中修改UI界面顯示操作結果;另一種是仍然在主線程中進行 NSTimer操作,但是將NSTimer實例加到main runloop的特定mode(模式)中。避免被複雜運算操作或者UI界面刷新所幹擾。

方法一:

在開始計時的地方:

1 if (self.timer) {2         [self.timer invalidate];3         self.timer = nil;4     }5     self.timer = [NSTimer timerWithTimeInterval:0.01 target:self selector:@selector(addTime) userInfo:nil repeats:YES];6     [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

[NSRunLoop currentRunLoop]獲取的就是“main runloop”,使用NSRunLoopCommonModes模式,將NSTimer加入其中。

(借鑑了博文:)

 

方法二:

開闢子線程:(使用子線程的runloop)

1 NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];2     [thread start];
1 - (void)newThread2 {3     @autoreleasepool4     {5         [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(addTime) userInfo:nil repeats:YES];6         [[NSRunLoop currentRunLoop] run];7     }8 }

在子線程中將NSTimer以默認方式加到該線程的runloop中,啓動子線程。

 

方法三:

使用GCD,同樣也是多線程方式:

聲明全局成員變量

1 dispatch_source_t _timers;
 1     uint64_t interval = 0.01 * NSEC_PER_SEC; 2     dispatch_queue_t queue = dispatch_queue_create("my queue", 0); 3     _timers = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); 4     dispatch_source_set_timer(_timers, dispatch_time(DISPATCH_TIME_NOW, 0), interval, 0); 5     __weak ViewController *blockSelf = self; 6     dispatch_source_set_event_handler(_timers, ^() 7     { 8         NSLog(@"Timer %@", [NSThread currentThread]); 9         [blockSelf addTime];10     });11     dispatch_resume(_timers);

然後在主線程中修改UI界面:

1 dispatch_async(dispatch_get_main_queue(), ^{2         self.label.text = [NSString stringWithFormat:@"%.2f", self.timeCount/100];3     });

遊戲源代碼可見:

 

總結:

runloop是一個看似很神祕的東西,其實一點也不神祕。每個線程都有一個實際已經存在的runloop。比如我們的主線程,在主函數的UIApplication中:

1 UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]))

系統就爲我們將主線程的main runloop隱式的啓動了。runloop顧名思義就是一個“循環”,他不停地運行,從程序開始到程序退出。正是由於這個“循環”在不斷地監聽各種事 件,程序纔有能力檢測到用戶的各種觸摸交互、網絡返回的數據纔會被檢測到、定時器纔會在預定的時間觸發操作……

runloop只接受兩種任務:輸入源和定時源。本文中說的就是定時源。默認狀態下,子線程的runloop中沒有加入我們自己的源,那麼我們在子線程中使用自己的定時器時,就需要自己加到runloop中,並啓動該子線程的runloop,這樣才能正確的運行定時器。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章