ObjC RunLoop簡析

SoC發表於2019-02-16

RunLoop,顧名思義就是執行迴圈的意思,是指程式在執行過程中迴圈做一些事情。

RunLoop 簡介

當我們建立一個terminal專案的時候,此時的main函式中並沒有一個RunLoop。所以程式執行完main函式之後就退出了。

沒有RunLoop

而一個iOS的application程式,預設在主執行緒開啟了一個RunLoop,這樣一個App就可以處理一些計時器事件,滑動事件等,不會馬上退出。

有RunLoop

在iOS專案中每一條執行緒都對應著一個RunLoop物件,RunLoop存放在一個以執行緒作為key的雜湊表中。

主執行緒在建立的時候預設開啟RunLoop,而其他子執行緒預設不開啟,但是會在第一次獲取RunLoop([NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent())的時候建立。

一般情況下RunLoop的生命週期跟隨執行緒,執行緒結束的時候RunLoop也會被銷燬。

iOS中提供了一套Foundation框架的NSRunLoop api和一套基於Core Foundation的CFRunLoopRef api來使用RunLoop。其中NSRunLoop是基於CFRunLoopRef做了一層OC的封裝。

RunLoop 結構

在CFRunLoop的原始碼中RunLoop的基本結構如下:

CFRunLoopRef

CFRunLoopRef是一個__CFRunLoop的結構體,結構體中存放了許多mode相關的成員。

其中_currentModeCFRunLoopModeRef型別的,它是一個__CFRunLoopMode型別的結構體指標。RunLoop通過它來表徵RunLoop的執行狀態。

一個RunLoop中包含有許多Mode。_commonModes是一個可變的集合,集合中存放了許多mode。RunLoop在執行的時候只能選擇一個Mode作為當前RunLoop執行的狀態,也就是_currentMode

mode是CFRunLoopMode型別的。而CFRunLoopMode是通過typedf__CFRunLoopMode得到的。__CFRunLoopMode中存放了處理觸控事件的source0、系統時間捕捉的source1、處理計時器的timers、監聽RunLoop狀態的observer等。

另外,如果RunLoop需要切換執行狀態的時候必須先退出當前的Mode,才能進入新的Mode。如果當前Mode中所有的sourcetimerobserver的時候RunLoop就會立刻退出。

常見的RunLoopMode有預設的modekCFRunLoopDefaultMode、跟蹤介面(比如:保證滑動不受其他mode影響)的UITrackingRunLoopMode。另外在api中還有一個kCFRunLoopCommonModes但是這並不是一個真正的mode,它不存在於_commonModes中,它只是一個標記。

RunLoop 的監聽器會監聽RunLoop的一些狀態:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), // 即將進入runloop
    kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理計時器
    kCFRunLoopBeforeSources = (1UL << 2), // 即將處理source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
    kCFRunLoopAfterWaiting = (1UL << 6), // 即將從休眠中喚醒
    kCFRunLoopExit = (1UL << 7), // 即將退出RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU // 全部狀態
};
複製程式碼

探究RunLoop的執行流程

我們可以通過Xcode自帶的lldb 通過bt命令檢視函式呼叫棧找到RunLoop的入口函式

RunLoop的入口函式

CFRunLoop.c檔案中找到該函式,我們發現它通過__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry)監聽進入RunLoop。接著有一個__CFRunLoopRun的函式呼叫,該函式中封裝了RunLoop處理事件的邏輯。

我們只關注__CFRunLoopRun主要程式碼:我們發現該函式中存在著一個do-while()迴圈,當retVal==0的時候迴圈持續進行,當retVal != 0的時候,回返回給函式CFRunLoopRunSpecific,它呼叫__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);退出RunLoop。

__CFRunLoopRun中主要的流程都在下圖中進行了描述。總結來說:

  1. 首先會通知監聽者Observers:即將處理Timers
  2. 通知監聽者Observers:即將處理Sources
  3. 處理blocks
  4. 處理source0,如果處理完了會再次處理blocks
  5. 如果存在source1,則跳到handle_msg處理,如果沒有則通知監聽器即將進入休眠
  6. 休眠時期等待訊息來喚醒當前執行緒
  7. 如果有訊息喚醒則進入handle_msg處理計時器,gcd,source1這些資訊
  8. 再次處理blocks
  9. 獲取返回值retVal
  10. 進入do-while(),如果retVal == 0 則迴圈持續進行。否則返回給CFRunLoopRunSpecific函式,退出RunLoop

RunLoop的執行流程

RunLoop的應用

Timer

當我們使用+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;建立一個計時器,並且頁面存在scrollView的時候。滑動scrollView計時器就會停止執行。這是因為一開始runloop存在於NSDefaultRunLoopMode,當滑動事件響應的時候runloop會進入UITrackingRunLoopMode模式處理滑動事件,所有timer就會失去處理。

我們可以使用+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;建立timer,然後將它放在一個NSRunLoopCommonModes標記的模式下進行工作,timer是可以t在_commonModes陣列中存放的模式下工作的。這樣就解決了滑動事件和timer計時器事件衝突的問題。

執行緒保活

當我們建立一條執行緒的時候,這條執行緒並沒有一個RunLoop。當我們第一次獲取RunLoop的時候這條執行緒中才會建立RunLoop。

所以建立一條一直存在的執行緒,我們需要線上程中加入一個不會被回收的RunLoop,也就是讓do-while()一直存在,也就是RunLoop一直有事情在處理,而retVal不會為不是0的其他值。

例項程式碼:

#import "SoCPermanentThread.h"
@interface SoCPermanentThread ()

@property (nonatomic, strong) NSThread *thread;

@end

@implementation SoCPermanentThread

- (instancetype)init {
    if (self = [super init]) {
        self.thread = [[NSThread alloc] initWithBlock:^{
            CFRunLoopSourceContext context = {0};
            CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
            CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
            CFRelease(source);
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
        }];
        [self.thread start];
    }
    return self;
}

- (void)executeTask:(SoCPermenantThreadTask)task {
    if (!_thread || !task) return;
    [self performSelector:@selector(__task:) onThread:_thread withObject:task waitUntilDone:NO];
}

- (void)stop {
    if (!_thread) return;
    [self performSelector:@selector(__stop) onThread:_thread withObject:nil waitUntilDone:YES];
}

- (void)__task:(SoCPermenantThreadTask)task {
    task();
}

- (void)__stop {
    CFRunLoopStop(CFRunLoopGetCurrent());
    _thread = nil;
}

- (void)dealloc {
    [self stop];
}

@end
複製程式碼

上述程式碼使用Core Foundation實現的執行緒保活,其中重要的就是首先往RunLoop中新增Source保證RunLoop有事情可以做,另外就是CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);這個方法的最後一個引數BOOL引數returnAfterSourceHandled,其值為flase代表執行完函式(處理完source)不會返回,而true則相反,表示執行完函式(處理完source)會立即返回。

總結

本篇主要以Core Foundation api 為基礎(Core Foundation開源)簡述了RunLoop的基本概念,和呼叫流程,由於NSRunLoop是基於CFRunLoop做的OC封裝,其原理和流程都是一樣的。另外介紹了兩個使用RunLoop的案例。