RunLoop概念與使用

1、RunLoop概念

RunLoop顧名思義就是能夠一直循環運行的機制。這種機制一般稱爲「消息循環機制」,其原理大體以下:html

void loop() {
    initialize();
    while(!quit) {
        id msg = get_next_message();
        process_message(msg);
    }
}
複製代碼

在iOS中,NSRunLoopCFRunLoopRef就是實現「消息循環機制」的對象。其實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

2、NSRunLoopMode

咱們在使用NSRunLoop時,會常常須要設置其mode屬性。常見的mode屬性主要包括:NSDefaultRunLoopModeUITrackingRunLoopModeNSRunLoopCommonModesgithub

程序應用大部分狀況下是處於NSDefaultRunLoopMode狀態,只有當scrollView滑動時,主線程RunLoop會自動切換爲UITrackingRunLoopMode狀態。安全

不一樣的mode影響到咱們設置的監聽者(好比TimerCADisplayLink)是否會被回調。好比在主線程中,設置TimerNSDefaultRunLoopMode屬性,當應用在滑動時,Timer的方法是不會被回調的,由於滑動過程當中,RunLoop會切換爲UITrackingRunLoopMode狀態,而它只是監聽了NSDefaultRunLoopMode狀態。app

在主線程中設置TimerCADisplayLink,咱們一般都會設置爲NSRunLoopCommonModes屬性,表示在NSDefaultRunLoopModeUITrackingRunLoopMode狀態下都會進行監聽,避免滑動時,沒法回調。異步

3、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是沒法主動被建立的,只能經過在currentRunLoopmainRunLoop獲取到對應的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.異步事件,一般爲addPortperformSelector:onThread方法;b.Timer事件,一般爲addTimerperformSelector: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設置的modeNSDefaultRunLoopMode,那麼是否是意味着當應用有scrollView滑動時,會致使沒法響應?答案是不會!這裏可能很容易產生一個誤解:只有mode設置爲NSRunLoopCommonModes,才能保證在scrollView滑動的狀況下也會響應。實際上是不對的,應該有個前提條件:主線程。由於只有mainRunLoop纔會在滑動時,切換爲UITrackingRunLoopMode,子線程中的RunLoop是不會的。

4、RunLoop系列文章

參考資料

相關文章
相關標籤/搜索