RunLoop
顧名思義就是能夠一直循環運行的機制。這種機制一般稱爲「消息循環機制」,其原理大體以下:html
void loop() {
initialize();
while(!quit) {
id msg = get_next_message();
process_message(msg);
}
}
複製代碼
在iOS中,
NSRunLoop
和CFRunLoopRef
就是實現「消息循環機制」的對象。其實NSRunLoop
本質是由CFRunLoopRef
封裝的,提供了面向對象的API,而CFRunLoopRef
是一些面向過程的C
函數API。二者最主要的區別在於:NSRunLoop
是非線程安全的,意味着你不能用非當前線程去調用當前線程的NSRunLoop
,不然會出現意想不到的錯誤(You should never try to call the methods of an NSRunLoop object running in a different thread)。而CFRunLoopRef
是線程安全的。git
NSRunLoopMode
咱們在使用
NSRunLoop
時,會常常須要設置其mode
屬性。常見的mode
屬性主要包括:NSDefaultRunLoopMode
、UITrackingRunLoopMode
和NSRunLoopCommonModes
。github
程序應用大部分狀況下是處於
NSDefaultRunLoopMode
狀態,只有當scrollView
滑動時,主線程RunLoop
會自動切換爲UITrackingRunLoopMode
狀態。安全
不一樣的
mode
影響到咱們設置的監聽者(好比Timer
或CADisplayLink
)是否會被回調。好比在主線程中,設置Timer
爲NSDefaultRunLoopMode
屬性,當應用在滑動時,Timer
的方法是不會被回調的,由於滑動過程當中,RunLoop
會切換爲UITrackingRunLoopMode
狀態,而它只是監聽了NSDefaultRunLoopMode
狀態。app
在主線程中設置
Timer
或CADisplayLink
,咱們一般都會設置爲NSRunLoopCommonModes
屬性,表示在NSDefaultRunLoopMode
和UITrackingRunLoopMode
狀態下都會進行監聽,避免滑動時,沒法回調。異步
NSRunLoop
的使用NSTimer
能夠嘗試將
NSRunLoopCommonModes
改爲NSDefaultRunLoopMode
,那麼timerFired:
函數在scrollview
滑動的時候,就不會被定時調用了,直到滑動中止。async
- (void)startTimer {
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)timerFired:(NSTimer *)timer {
NSLog(@"fired timer in %@", [NSDate date]);
}
複製代碼
CADisplayLink
- (void)startDisplayLink {
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTick:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)displayLinkTick:(CADisplayLink *)link {
NSLog(@"tick display link in %@", [NSDate date]);
}
複製代碼
performSelector:withObject:afterDelay:
這裏看似並無使用到
NSRunLoop
,但實際上是它內部會建立一個Timer
,並加Timer
加入到當前線程對應的NSRunLoop
中(This method sets up a timer to perform the aSelector message on the current thread’s run loop. )。函數
- (void)performSel {
[self performSelector:@selector(performSelFired:) withObject:@"perform" afterDelay:3.0 inModes:@[NSRunLoopCommonModes]];
NSLog(@"performSelector start in %@", [NSDate date]);
}
- (void)performSelFired:(NSString *)object {
NSLog(@"performSelector with obj: %@ in %@", object, [NSDate date]);
}
複製代碼
NSRunLoop
- (void)performInThread {
__weak typeof(self) wSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"performInThread start in %@", [NSDate date]);
[wSelf performSelector:@selector(threadFired:) withObject:@"thread perform" afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
});
}
- (void)threadFired:(NSString *)object {
NSLog(@"performInThread with obj: %@ in %@", object, [NSDate date]);
}
複製代碼
運行該代碼,會發現threadFired
方法並不會調用。爲什麼在子線程就沒法生效呢?oop
a. 線程和RunLoop
是一一對應的,且互相獨立,好比主線程對應mainRunLoop
,而子線程也是有它本身所對應的RunLoop
。 b. 主線程的RunLoop
在應用啓動的時候就開始run
了,而子線程是須要主動調用其run
方法來啓動。post
- (void)performInThread {
__weak typeof(self) wSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"performInThread start in %@", [NSDate date]);
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[wSelf performSelector:@selector(threadFired:) withObject:@"thread perform" afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
[runLoop run];
});
}
複製代碼
獲取到子線程對應的RunLoop
後,調用其run
方法就能夠看到threadFired
被調用了。注意:RunLoop
是沒法主動被建立的,只能經過在currentRunLoop
或mainRunLoop
獲取到對應的RunLoop
。
假設在這裏作一個修改,將[runLoop run];
方法提早,以下:
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop run];
[wSelf performSelector:@selector(threadFired:) withObject:@"thread perform" afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
複製代碼
修改後,會發現threadFired
函數又沒法被調用了。這又是什麼緣由?
這時由於NSRunLoop
是須要source event
纔會一直運行的,不然運行完會被終止。這裏一般會有兩種source event
:a.異步事件,一般爲addPort
或performSelector:onThread
方法;b.Timer事件
,一般爲addTimer
或performSelector:afterDelay
等方法。
因此,提早調用run
方法時,RunLoop
沒有設置任何source event
,因此會當即終止,而執行到下面的performSelector
方法時,這時雖然設置了timer source
,但RunLoop
已經終止,天然也就沒法響應了。
addPort
經過
addPort
方法可使RunLoop
監聽某個端口的事件,從而保證其一直運行。
- (void)addPort {
__weak typeof(self) wSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"start run addPort in %@", [NSDate date]);
wSelf.thread = [NSThread currentThread];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
});
for (NSInteger i = 1; i <= 3; i ++) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * i * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"start receive port msg in %@", [NSDate date]);
[wSelf performSelector:@selector(receiveMsg) onThread:wSelf.thread withObject:nil waitUntilDone:NO];
});
}
}
- (void)receiveMsg {
NSLog(@"receive msg in thread in %@", [NSDate date]);
}
複製代碼
這裏經過註冊NSMachPort
端口,來保證該線程的RunLoop
一直處於運行狀態。
這裏有個問題,NSRunLoop
設置的mode
爲NSDefaultRunLoopMode
,那麼是否是意味着當應用有scrollView
滑動時,會致使沒法響應?答案是不會!這裏可能很容易產生一個誤解:只有mode
設置爲NSRunLoopCommonModes
,才能保證在scrollView
滑動的狀況下也會響應。實際上是不對的,應該有個前提條件:主線程。由於只有mainRunLoop
纔會在滑動時,切換爲UITrackingRunLoopMode
,子線程中的RunLoop
是不會的。