游戲中有一個計時功能。在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)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 });
游戲源代碼可見:
總結:
runloop是一個看似很神秘的東西,其實一點也不神秘。每個線程都有一個實際已經存在的runloop。比如我們的主線程,在主函數的UIApplication中:
1 UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]))
系統就為我們將主線程的main runloop隱式的啟動了。runloop顧名思義就是一個“循環”,他不停地運行,從程序開始到程序退出。正是由於這個“循環”在不斷地監聽各種事件,程序才有能力檢測到用戶的各種觸摸交互、網絡返回的數據才會被檢測到、定時器才會在預定的時間觸發操作……
runloop只接受兩種任務:輸入源和定時源。本文中說的就是定時源。默認狀態下,子線程的runloop中沒有加入我們自己的源,那么我們在子線程中使用自己的定時器時,就需要自己加到runloop中,並啟動該子線程的runloop,這樣才能正確的運行定時器。