這篇文章是我的【iOS開發每日小筆記】系列中的一片,記錄的是今天在開發工作中遇到的,可以用很短的文章或很小的demo演示解釋出來的小心得小技巧。它們可能會給用戶體驗、代碼效率得到一些提升,或是之前自己沒有接觸過的技術,很開心的學到了,放在這里得瑟一下。90%的作用是幫助自己回顧、記憶、復習。
一直想寫一篇關於runloop學習有所得的文章,總是沒有很好的例子。正巧自己的上線App Store的小游戲《跑酷好基友》(https://itunes.apple.com/us/app/pao-ku-hao-ji-you/id914554369?mt=8)中有一個很好的實際使用例子。游戲中有一個計時功能。在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加入其中。
(借鑒了博文:http://www.mgenware.com/blog/?p=459)
方法二:
開辟子線程:(使用子線程的runloop)
1 NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil]; 2 [thread start];
1 - (void)newThread 2 { 3 @autoreleasepool 4 { 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 });
游戲源代碼可見:https://github.com/pigpigdaddy/BothLive
總結:
runloop是一個看似很神秘的東西,其實一點也不神秘。每個線程都有一個實際已經存在的runloop。比如我們的主線程,在主函數的UIApplication中:
1 UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]))
系統就為我們將主線程的main runloop隱式的啟動了。runloop顧名思義就是一個“循環”,他不停地運行,從程序開始到程序退出。正是由於這個“循環”在不斷地監聽各種事件,程序才有能力檢測到用戶的各種觸摸交互、網絡返回的數據才會被檢測到、定時器才會在預定的時間觸發操作……
runloop只接受兩種任務:輸入源和定時源。本文中說的就是定時源。默認狀態下,子線程的runloop中沒有加入我們自己的源,那么我們在子線程中使用自己的定時器時,就需要自己加到runloop中,並啟動該子線程的runloop,這樣才能正確的運行定時器。
參考文章:
http://www.mgenware.com/blog/?p=459
http://blog.csdn.net/wzzvictory/article/details/9237973
http://www.dahuangphone.com/dv_rss.asp?s=xhtml&boardid=8&id=93&page=5