1. 程式人生 > >在子執行緒中使用runloop,正確操作NSTimer計時的注意點 三種可選方法

在子執行緒中使用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)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,這樣才能正確的執行定時器。