NSTimer 的 scheduledTimerWithTimeInterval 方法使用時需要在主線程中使用否則不會執行的。
1. NSRunLoopCommonModes和Timer
當使用NSTimer
的scheduledTimerWithTimeInterval
方法時。事實上此時Timer會被加入到當前線程的Run
Loop中,且模式是默認的NSDefaultRunLoopMode
。而如果當前線程就是主線程,也就是UI線程時,某些UI事件,比如UIScrollView
的拖動操作,會將Run
Loop切換成NSEventTrackingRunLoopMode
模式,在這個過程中,默認的NSDefaultRunLoopMode
模式中註冊的事件是不會被執行的。也就是說,此時使用scheduledTimerWithTimeInterval
添加到Run
Loop中的Timer就不會執行。
所以爲了設置一個不被UI干擾的Timer,我們需要手動創建一個Timer,然後使用NSRunLoop
的addTimer:forMode:
方法來把Timer按照指定模式加入到Run
Loop中。這裏使用的模式是:NSRunLoopCommonModes
,這個模式等效於NSDefaultRunLoopMode
和NSEventTrackingRunLoopMode
的結合。(參考Apple文檔)
參考代碼:
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"主線程 %@", [NSThread currentThread]); //創建Timer NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timer_callback) userInfo:nil repeats:YES]; //使用NSRunLoopCommonModes模式,把timer加入到當前Run Loop中。 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; } //timer的回調方法 - (void)timer_callback { NSLog(@"Timer %@", [NSThread currentThread]); }
輸出:
主線程 <NSThread: 0x71501e0>{name = (null), num = 1} Timer <NSThread: 0x71501e0>{name = (null), num = 1} Timer <NSThread: 0x71501e0>{name = (null), num = 1} Timer <NSThread: 0x71501e0>{name = (null), num = 1}
2. NSThread和Timer
上面講的NSRunLoopCommonModes
和Timer中有一個問題,這個Timer本質上是在當前線程的Run
Loop中循環執行的,因此Timer的回調方法不是在另一個線程的。那麼怎樣在真正的多線程環境下運行一個Timer呢?
可以先試試NSThread
。同上,我們還是會把Timer加到Run
Loop中,只不過這個是在另一個線程中,因此我們需要手動執行Run Loop(通過NSRunLoop
的run
方法),同時注意在新的線程執行中加入@autoreleasepool
。
完整代碼如下:
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"主線程 %@", [NSThread currentThread]); //創建並執行新的線程 NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil]; [thread start]; } - (void)newThread { @autoreleasepool { //在當前Run Loop中添加timer,模式是默認的NSDefaultRunLoopMode [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(timer_callback) userInfo:nil repeats:YES]; //開始執行新線程的Run Loop [[NSRunLoop currentRunLoop] run]; } } //timer的回調方法 - (void)timer_callback { NSLog(@"Timer %@", [NSThread currentThread]); }
輸出:
主線程 <NSThread: 0x7118800>{name = (null), num = 1} Timer <NSThread: 0x715c2e0>{name = (null), num = 3} Timer <NSThread: 0x715c2e0>{name = (null), num = 3} Timer <NSThread: 0x715c2e0>{name = (null), num = 3}
3. GCD中的Timer
GCD中的Timer應該是最靈活的,而且是多線程的。GCD中的Timer是靠Dispatch Source來實現的。
因此先需要聲明一個dispatch_source_t
本地變量:
@interface ViewController () { dispatch_source_t _timer; }
接着通過dispatch_source_create
函數來創建一個專門的Dispatch
Source,接着通過dispatch_source_set_timer
函數來設置Timer的參數,注意這裏的時間參數有些蛋疼。
開始時間的類型是dispatch_time_t
,最好用dispatch_time
或者dispatch_walltime
函數來創建dispatch_time_t
對象。如果需要Timer立即執行,可以傳入dispatch_time(DISPATCH_TIME_NOW,
0)
。
internal
和leeway
參數分別表示Timer的間隔時間和精度。類型都是uint64_t
。間隔時間的單位竟然是納秒。可以藉助預定義的NSEC_PER_SEC
宏,比如如果間隔時間是兩秒的話,那interval
參數就是:2
* NSEC_PER_SEC
。
leeway
就是精度參數,代表系統可以延時的時間間隔,最高精度當然就傳0。
然後通過dispatch_source_set_event_handler
函數來設置Dispatch
Source的事件回調,這裏當然是使用Block了。
最後所有dispatch_source_t
創建後默認都是暫停狀態的,所以必須通過dispatch_resume
函數來開始事件監聽。這裏就代表着開始Timer。
完整代碼:
NSLog(@"主線程 %@", [NSThread currentThread]); //間隔還是2秒 uint64_t interval = 2 * NSEC_PER_SEC; //創建一個專門執行timer回調的GCD隊列 dispatch_queue_t queue = dispatch_queue_create("my queue", 0); //創建Timer _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); //使用dispatch_source_set_timer函數設置timer參數 dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 0), interval, 0); //設置回調 dispatch_source_set_event_handler(_timer, ^() { NSLog(@"Timer %@", [NSThread currentThread]); }); //dispatch_source默認是Suspended狀態,通過dispatch_resume函數開始它 dispatch_resume(_timer);
輸出:
主線程 <NSThread: 0x711fab0>{name = (null), num = 1} Timer <NSThread: 0x713a380>{name = (null), num = 3} Timer <NSThread: 0x713a380>{name = (null), num = 3} Timer <NSThread: 0x713a380>{name = (null), num = 3}