RunLoop的一些學習與總結

weixin_33978044發表於2017-01-20

最近在學習一些OC底層的東西, 下面是學習了RunLoop的一些總結和感受^^

首先,RunLoop的作用

從字面上看,RunLoop是執行迴圈的意思.實際上,執行緒就是依靠RunLoop來實現任務來了被喚醒,完成任務之後休眠的這一特性.

RunLoop的基本作用:

  • 保持程式的持續執行
  • 處理App中的各種事件(比如觸控事件, 定時器事件, selector事件)
  • 節約系統資源,提高程式效能(有事件要處理時被喚醒,沒事件時休眠)

RunLoop實際上就是一個物件,這個物件管理了其需要處理的事件和訊息,並提供了一個入口函式來執行任務來了被喚醒,完成任務之後休眠的邏輯。執行緒執行了這個函式後,就會一直處於這個函式內部 "接受訊息->等待->處理" 的迴圈中,直到這個迴圈結束(比如傳入quit的訊息),函式返回。

OSX/iOS 系統中,提供了兩個這樣的物件:NSRunLoopCFRunLoopRef
CFRunLoopRef是在CoreFoundation框架內的,它提供了純 C 函式的 API,所有這些 API 都是執行緒安全的。
NSRunLoop是基於CFRunLoopRef的封裝,提供了物件導向的 API,但是這些 API 不是執行緒安全的。

RunLoop與執行緒的關係

蘋果不允許直接建立RunLoop,它只提供了兩個自動獲取的函式:CFRunLoopGetMain()CFRunLoopGetCurrent()。這兩個函式內部的邏輯大概是下面這樣:

/// 全域性的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
 
/// 獲取一個 pthread 對應的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次進入時,初始化全域性Dic,並先為主執行緒建立一個 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接從 Dictionary 裡獲取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到時,建立一個
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 註冊一個回撥,當執行緒銷燬時,順便也銷燬其對應的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

從上面的程式碼中,我們可以得到以下的一些結論:

  • 每條執行緒都有唯一的一個與之對應的RunLoop物件,並且以{pthread_t : CFRunLoopRef}的形式儲存在一個全域性的Dictionary中
  • 主執行緒的RunLoop已經自動建立好了,子執行緒的RunLoop需要手動進行建立
  • RunLoop在第一次獲取時通過懶載入建立,線上程結束時銷燬
  • 你只能在一個執行緒內部獲取屬於它自己的那個RunLoop(主執行緒除外)
function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

RunLoop相關類

Core Foundation中關於RunLoop的5個類

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

他們的關係關係如圖:

1929756-3fe773d36a4fdf66.png
RunLoop_0.png

一個 RunLoop 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer。每次呼叫 RunLoop 的主函式時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode。如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進入。這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響。

CFRunLoopSourceRef

CFRunLoopSourceRef是事件源(輸入源)

以前的分法

  • Port-Based Sources
  • Custom Input Sources
  • Cocoa Perform Selector Sources

現在的分法

  • Source0:只包含了一個回撥(函式指標),它並不能主動觸發事件。使用時,你需要先呼叫 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然後手動呼叫 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。
  • Source1:包含了一個 mach_port和一個回撥(函式指標),被用於通過核心和其他執行緒相互傳送訊息

可以以下程式碼新增新的CFRunLoopSourceRef:

[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

CFRunLoopTimerRef

CFRunLoopTimerRef是基於時間的觸發器,它和 NSTimertoll-free bridged 的,可以混用。其包含一個時間長度和一個回撥(函式指標)。當其加入到 RunLoop 時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回撥。

  • NSTimer會受到RunLoop的Mode的影響可能會不準確
  • GCD的定時器不受RunLoop的Mode的影響

下面是兩種往RunLoop中新增定時器的方法:

- (void)testRunloopTimerWithNSTimer
{
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"我是NSTimer");
    }];
    //預設模式
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    //滑動模式
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    //能夠在預設模式或者滑動模式中回撥這個NSTimer
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

- (void)testRunloopTimerWithGCDTimer
{
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, mainQueue);
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"我是GCD Timer");
    });
    dispatch_resume(timer);
    //需要要有一個指標指向Timer,不然這個函式結束Timer就被釋放掉了
    self.timer = timer;
}

CFRunLoopObserverRef

CFRunLoopObserverRef是觀察者,能夠監聽RunLoop狀態變化. 能夠觀測的時間點有以下幾個:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即將進入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即將處理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 剛從休眠中喚醒
    kCFRunLoopExit          = (1UL << 7), // 即將退出Loop
};

Runloop的Mode

上面的 Source/Timer/Observer 被統稱為 mode item,一個item可以被同時加入多個mode。但一個 item被重複加入同一個mode 時是不會有效果的。如果一個 mode 中一個 item都沒有,則 RunLoop會直接退出,不進入迴圈。

CFRunLoopMode 和 CFRunLoop 的結構大致如下:

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};
 
struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

幾種常使用的Mode:

  • NSDefaultRunLoopMode:這是App平時所處的狀態
  • UITrackingRunLoopMode:這是追蹤 ScrollView滑動時的狀態
  • NSRunLoopCommonModes:能夠將mode itemr新增到__CFRunLoop裡面的_commonModeItems中,然後RunLoop會將會自動將這個mode item同步到具有 "Common"標記的所有Mode(NSDefaultRunLoopMode和UITrackingRunLoopMode)裡。

RunLoop的內部邏輯

其內部程式碼整理如下:

/// 用DefaultMode啟動
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
 
/// 用指定的Mode啟動,允許設定RunLoop超時時間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
 
/// RunLoop的實現
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
    
    /// 首先根據modeName找到對應mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    /// 如果mode裡沒有source/timer/observer, 直接返回。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;
    
    /// 1. 通知 Observers: RunLoop 即將進入 loop。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
    
    /// 內部函式,進入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
        
        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {
 
            /// 2. 通知 Observers: RunLoop 即將觸發 Timer 回撥。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            /// 3. 通知 Observers: RunLoop 即將觸發 Source0 (非port) 回撥。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            /// 執行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
            /// 4. RunLoop 觸發 Source0 (非port) 回撥。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            /// 執行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
 
            /// 5. 如果有 Source1 (基於port) 處於 ready 狀態,直接處理這個 Source1 然後跳轉去處理訊息。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }
            
            /// 通知 Observers: RunLoop 的執行緒即將進入休眠(sleep)。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }
            
            /// 7. 呼叫 mach_msg 等待接受 mach_port 的訊息。執行緒將進入休眠, 直到被下面某一個事件喚醒。
            /// • 一個基於 port 的Source 的事件。
            /// • 一個 Timer 到時間了
            /// • RunLoop 自身的超時時間到了
            /// • 被其他什麼呼叫者手動喚醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }
 
            /// 8. 通知 Observers: RunLoop 的執行緒剛剛被喚醒了。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
            
            /// 收到訊息,處理訊息。
            handle_msg:
 
            /// 9.1 如果一個 Timer 到時間了,觸發這個Timer的回撥。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 
 
            /// 9.2 如果有dispatch到main_queue的block,執行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 
 
            /// 9.3 如果一個 Source1 (基於port) 發出事件了,處理這個事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }
            
            /// 執行加入到Loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
 
            if (sourceHandledThisLoop && stopAfterHandle) {
                /// 進入loop時引數說處理完事件就返回。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出傳入引數標記的超時時間了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部呼叫者強制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                /// source/timer/observer一個都沒有了
                retVal = kCFRunLoopRunFinished;
            }
            
            /// 如果沒超時,mode裡沒空,loop也沒被停止,那繼續loop。
        } while (retVal == 0);
    }
    
    /// 10. 通知 Observers: RunLoop 即將退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

可以看到,實際上RunLoop 就是這樣一個函式,其內部是一個 do-while 迴圈。當你呼叫CFRunLoopRun()時,執行緒就會一直停留在這個迴圈裡;直到超時或被手動停止,該函式才會返回。

應用

  • 使用執行緒的定時器

  • Cocoa中使用任何performSelector…的方法

  • 自動釋放池

App啟動後,蘋果在主執行緒 RunLoop 裡註冊了兩個 Observer,其回撥都是_wrapRunLoopWithAutoreleasePoolHandler()

第一個 Observer 監視的事件是 Entry(即將進入Loop),其回撥內會呼叫 _objc_autoreleasePoolPush() 建立自動釋放池。其 order 是-2147483647,優先順序最高,保證建立釋放池發生在其他所有回撥之前。

第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時呼叫_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()釋放舊的池並建立新池;Exit(即將退出Loop) 時呼叫 _objc_autoreleasePoolPop()來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先順序最低,保證其釋放池子發生在其他所有回撥之後。

在主執行緒執行的程式碼,通常是寫在諸如事件回撥、Timer回撥內的。這些回撥會被 RunLoop 建立好的 AutoreleasePool 環繞著,所以不會出現記憶體洩漏,開發者也不必顯示建立 Pool了。

  • 建立一個常駐執行緒

開啟一個常駐執行緒(讓一個子執行緒不進入消亡狀態,等待其他執行緒發來訊息,處理其他事件),可以在這個執行緒裡面開啟一個定時器或者進行一些長期監控

通過方法[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];在RunLoop中新增至少一個Source就能夠讓執行緒常駐

  • 可以新增Observer監聽RunLoop的狀態,比如監聽點選事件的處理(在所有點選事件之前做一些事情)

參考資料

ibireme深入理解RunLoop

相關文章