RunLoop 淺析

RunLoop 淺析

一個小應用

首先我們需要編寫一個應用,這個小應用的要求很簡單:它需要執(zhí)行一些比較耗時的操作,在執(zhí)行耗時操作的同時還需要可以繼續(xù)響應用戶的操作。

那么首先想到的就是使用兩個線程,一個 Main 一個 Worker,在 Main 中響應用戶的操作,而將實際的耗時任務放到 Worker 中。

首先看看在不使用 RunLoop 時的代碼是如何實現(xiàn)的:

//
//  main.m
//  Downloader
//
//  Created by mconintet on 11/23/15.
//  Copyright ? 2015 mconintet. All rights reserved.
//

#import <Foundation/Foundation.h>

// 『消息隊列(messages queue)』這個名詞想必是家喻戶曉了
// 這里 commands 就相當于一個消息隊列的作用
// 主線程在收到了用戶的 command 之后并不是
// 立即處理它們,轉而將其添加到這個 queue 中,
// 然后 Worker 會逐個的處理這個命令
static NSMutableArray* commands;

// NSMutableArray 并不是 thread-safety,所以
// 需要 @synchronized 來保證數(shù)據(jù)完整性
void pushCommand(NSString* cmd)
{
    @synchronized(commands)
    {
        [commands addObject:cmd];
    }
}

NSString* popCommand()
{
    @synchronized(commands)
    {
        NSString* ret = [commands lastObject];
        [commands removeLastObject];
        return ret;
    }
}

@interface Worker : NSThread

@end

@implementation Worker

- (void)main
{
    // 如你所見,在 Worker 中我們
    // 采用了『輪詢』的方式,就是不斷的
    // 詢問消息隊列,是不是有新消息來了
    while (1) {
        NSString* last = popCommand();
        // 如果通過不斷的輪詢得到新的命令
        // 那么就處理那個命令
        while (last) {
            NSLog(@"[Worker] executing command: %@", last);
            sleep(2); // 模擬耗時的計算所需的時間
            NSLog(@"[Worker] executed command: %@", last);
            last = popCommand();
        }
    }
}

@end

int main(int argc, const char* argv[])
{
    @autoreleasepool
    {
        commands = [[NSMutableArray alloc] init];

        Worker* worker = [[Worker alloc] init];
        [worker start];

        int c = 0;
        do {
            c = getchar();
            // 忽略輸入的換行
            // 這樣 Log 內(nèi)容更加清晰
            if (c == '\n')
                continue;

            NSString* cmd = [NSString stringWithCharacters:(const unichar*)&c length:1];
            pushCommand(cmd);
            // 在主線程中 Log 這條信息,
            // 以此來表示主線程可以繼續(xù)響應
            NSLog(@"[Main] added new command: %@", cmd);
        } while (c != 'q');
    }
    return 0;
}

運行下這個程序,然后切換到 Debug navigator,會看到這樣的結果:

Worker 讓 CPU 幾乎滿了 ??,看來 Worker 輪詢消息隊列的方式有很大的性能問題?;乜?Worker 中這樣的代碼:

while (1) {
    NSString* last = popCommand();
    while (last) {
        NSLog(@"executint command: %@", last);
        sleep(2); // 模擬耗時的計算所需的時間
        NSLog(@"executed command: %@", last);
        last = popCommand();
    }
}

上面代碼作用就是采用輪詢的方式不斷的向消息隊列詢問是否有新消息到達。這樣的模式會有一個嚴重的問題:如果在很長一段時間內(nèi)用戶并沒有輸入新的 command,子線程還是會不斷的輪詢,就是因為這些不斷的輪詢導致 CPU 資源被占滿。

Worker 不斷輪詢消息隊列的模式已經(jīng)被我們證明是具有性能問題的了,那么是不是可以換一種思路?如果可以讓 Main 和 Worker 的協(xié)作變?yōu)檫@樣:

  1. Main 不斷地接收到用戶輸入,將輸入放到消息隊列中,然后通知 Worker 說『Wake up,你有新的任務需要處理』
  2. Worker 開始處理消息隊列中任務,任務處理完成之后,自動進入休眠,不再繼續(xù)占用 CPU 資源,直到接收到下一次 Main 的通知

為了完成這個模式,我們可以采用 RunLoop。

RunLoop

在使用 RunLoop 之前,先了解下它。具體的在 Run Loops,扼要的說:

  1. 每個線程都有一個與之相關的 RunLoop
  2. 與線程相關聯(lián)的 RunLoop 需要手動的運行,以此讓其開始處理任務。主線程已經(jīng)為你自動的啟動了與其關聯(lián)的 RunLoop(注意命令行程序的主線程并沒有這個自動開啟的動作)
  3. RunLoop 需要以特定的 mode 去運行?!篶ommon mode』實際上是一組 modes,有相關的 API 可以向其中添加 mode
  4. RunLoop 的目的就是監(jiān)控 timers 和 run loop sources。每一個 run loop source 需要注冊到特定的 run loop 的特定 mode 上,并且只有當 run loop 運行在相應的 mode 上時,mode 中的 run loop source 才有機會在其準備好時被 run loop 所觸發(fā)
  5. RunLoop 在其每一次的循環(huán)中,都會經(jīng)歷幾個不同的場景,比如檢查 timers、檢查其他的 event sources。如果有需要被觸發(fā)的 source,那么會觸發(fā)與那個 source 相關的 callback
  6. 除了使用 run loop source 之外,還可以創(chuàng)建 run loop observers 來追蹤 run loop 的處理進度

如果要更加深入的了解 RunLoop 推薦閱讀 深入理解RunLoop。

使用 RunLoop 來改寫程序

下面的代碼使用 RunLoop 來改寫上面的程序:

//
//  main.m
//  Downloader
//
//  Created by mconintet on 11/23/15.
//  Copyright ? 2015 mconintet. All rights reserved.
//

#import <Foundation/Foundation.h>

static NSMutableArray* commands;

void pushCommand(NSString* cmd)
{
    @synchronized(commands)
    {
        [commands addObject:cmd];
    }
}

NSString* popCommand()
{
    @synchronized(commands)
    {
        NSString* ret = [commands lastObject];
        [commands removeLastObject];
        return ret;
    }
}

// run loop source 相關的回調(diào)函數(shù)
// 在外部代碼標記了 run loop 中的某個 run loop source
// 是 ready-to-be-fired 時,那么在未來的某一時刻 run loop
// 發(fā)現(xiàn)該 run loop source 需要被觸發(fā),那么就會調(diào)用到這個與其
// 相關的回調(diào)
void RunLoopSourcePerformRoutine(void* info)
{
    // 如果該方法被調(diào)用,那么說明其相關的 run loop source
    // 已經(jīng)準備好。在這個程序中就是 Main 通知了 Worker 『任務來了』
    NSString* last = popCommand();
    while (last) {
        NSLog(@"[Worker] executing command: %@", last);
        sleep(2); // 模擬耗時的計算所需的時間
        NSLog(@"[Worker] executed command: %@", last);
        last = popCommand();
    }
}

// Main 除了需要標記相關的 run loop source 是 ready-to-be-fired 之外,
// 還需要調(diào)用 CFRunLoopWakeUp 來喚醒指定的 RunLoop
// RunLoop 是不能手動創(chuàng)建的,所以必須注冊這個回調(diào)來向 Main 暴露 Worker
// 的 RunLoop,這樣在 Main 中才知道要喚醒誰
static CFRunLoopRef workerRunLoop = nil;
// 這也是一個 run loop source 相關的回調(diào),它發(fā)生在 run loop source 被添加到
// run loop 時,通過注冊這個回調(diào)來獲取 Worker 的 run loop
void RunLoopSourceScheduleRoutine(void* info, CFRunLoopRef rl, CFStringRef mode)
{
    workerRunLoop = rl;
}

@interface Worker : NSThread
@property (nonatomic, assign) CFRunLoopSourceRef rlSource;
@end

@implementation Worker

- (instancetype)initWithRunLoopSource:(CFRunLoopSourceRef)rlSource
{
    if ((self = [super init])) {
        _rlSource = rlSource;
    }
    return self;
}

- (void)main
{
    NSLog(@"[Worker] is running...");
    // 往 RunLoop 中添加 run loop source
    // 我們的 Main 會通過 rls 和 Worker 協(xié)調(diào)工作
    CFRunLoopAddSource(CFRunLoopGetCurrent(), _rlSource, kCFRunLoopDefaultMode);
    // 線程需要手動運行 RunLoop
    CFRunLoopRun();
    NSLog(@"[Worker] is stopping...");
}

@end

// 告訴 Worker 任務來了
// 把 Worker 拎起來干事
void notifyWorker(CFRunLoopSourceRef rlSource)
{
    if (workerRunLoop) {
        CFRunLoopSourceSignal(rlSource);
        CFRunLoopWakeUp(workerRunLoop);
    }
}

int main(int argc, const char* argv[])
{
    @autoreleasepool
    {
        NSLog(@"[Main] is running...");

        commands = [[NSMutableArray alloc] init];

        // run loop source 的上下文
        // 就是一些 run loop source 相關的選項以及回調(diào)
        // 另外我們這的第一個參數(shù)是 0,必須是 0
        // 這樣創(chuàng)建的 run loop source 就被添加在
        // run loop 中的 _sources0,作為用戶創(chuàng)建的
        // 非自動觸發(fā)的
        CFRunLoopSourceContext context = {
            0, NULL, NULL, NULL, NULL, NULL, NULL,
            RunLoopSourceScheduleRoutine,
            NULL,
            RunLoopSourcePerformRoutine
        };

        CFRunLoopSourceRef runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);

        Worker* worker = [[Worker alloc] initWithRunLoopSource:runLoopSource];
        [worker start];

        int c = 0;
        do {
            c = getchar();
            if (c == '\n')
                continue;

            NSString* cmd = [NSString stringWithCharacters:(const unichar*)&c length:1];
            pushCommand(cmd);
            NSLog(@"[Main] added new command: %@", cmd);

            notifyWorker(runLoopSource);
        } while (c != 'q');

        NSLog(@"[Main] is stopping...");
    }
    return 0;
}

可以運行一下看下性能如何:

可以看到,在沒有新的用戶輸入到達,且消息隊列中沒有需要處理的任務時,整個應用程序沒有持續(xù)的霸占 CPU 資源,這就歸功于 RunLoop。

最后簡單概括下為什么 RunLoop 有這么『神奇』的功能吧。

首先 RunLoop 內(nèi)部核心也是一個 loop 循環(huán)(和它的名字呼應),然后這個循環(huán)中做了一些有意思的事情:

  1. 首先每一次的循環(huán)中,都會檢查被添加到其中的 timers 和 run loop sources,如果它們之中有符合條件的,那么自然是需要觸發(fā)相關的回調(diào)操作
  2. 如果沒有 timers 或者 run loop sources 或者 run loop 被手動的停止了 那么 run loop 會退出內(nèi)部的循環(huán)
  3. 如果被添加到內(nèi)部的 timers 和 run loop sources 都沒有準備好被觸發(fā),那么 run loop 就會進行一個系統(tǒng)調(diào)用,使線程進入休眠
  4. 進入休眠了就不會占用 CPU 資源,那么喚醒的工作就需要其外部的代碼進行,比如上面代碼中 Main 中的 notifyWorker

這都是嘛

有這么幾個名詞真是非常的饒人:RunLoop、RunLoop Source、RunLoop Mode、CommonMode ...

『這些都是嘛?』這就是我剛見到它們的感覺,如果你也有這樣的感覺,那么再次推薦你先看下 深入理解RunLoop,我也是看了其中內(nèi)容,然后下載了 RunLoop 的源碼,自己動手分析分析,接下來將是我分析的備忘。

首先是看下 RunLoop 的結構:

struct __CFRunLoop {
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
}

于是看到,與 RunLoop 有直接關系的是 RunLoop Mode。那么看看 RunLoop Mode 的結構:

struct __CFRunLoopMode {
    CFStringRef _name;
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
}

發(fā)現(xiàn)與 RunLoop Mode 有關的是 RunLoop sourcetimer 以及 observer。

于是就有了這個圖:

+---------------------------------------------------------+
|                                                         |
|                        RunLoop                          |
|                                                         |
|  +----------------------+    +----------------------+   |
|  |                      |    |                      |   |
|  |     RunLoopMode      |    |     RunLoopMode      |   |
|  |                      |    |                      |   |
|  |  +----------------+  |    |  +----------------+  |   |
|  |  | RunLoopSources |  |    |  | RunLoopSources |  |   |
|  |  +----------------+  |    |  +----------------+  |   |
|  |                      |    |                      |   |
|  |    +-----------+     |    |    +-----------+     |   |
|  |    | Observers |     |    |    | Observers |     |   |
|  |    +-----------+     |    |    +-----------+     |   |
|  |                      |    |                      |   |
|  |      +--------+      |    |      +--------+      |   |
|  |      | Timers |      |    |      | Timers |      |   |
|  |      +--------+      |    |      +--------+      |   |
|  |                      |    |                      |   |
|  +----------------------+    +----------------------+   |
|                                                         |
+---------------------------------------------------------+

然后看看 Common Mode 是干什么的,首先看看這個函數(shù):

void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName);

就是往 RunLoop 中添加 Common Mode,而 Common Mode 在 RunLoop 中以 Set 的結構去存放(見上面 RunLoop 數(shù)據(jù)結構中的 CFMutableSetRef _commonModes;),也就是 RunLoop 中可以有多個 Common Mode,而且注意到添加時是以 Mode Name 去代表具體的 Mode 的。

然后再看下這個函數(shù):

void CFRunLoopAddSource(
    CFRunLoopRef rl, 
    CFRunLoopSourceRef rls, 
    CFStringRef modeName
);

這里就不放函數(shù)體了,有興趣的可以下載源碼去看,大概的意思就是:

如果 CFRunLoopAddSource 被調(diào)用時,形參 modeName 的實參值為 kCFRunLoopCommonModes 時,就會將 rls 添加到 RunLoop 中的 _commonModeItems 中。上面我知道了 _commonModes 其實是一個 Set,里面存放的是 Mode Names,于是下一步 RunLoop 就會迭代 _commonModes 這個 Set 中的元素。對于迭代時的元素,很明顯都是 Mode Name,然后通過 __CFRunLoopFindMode 方法,根據(jù) Mode Name 找出存儲在 RunLopp 中的 _modes 中的 Mode,然后將 rls 添加到那些 Mode 中。

如果覺得很亂的話,只要知道為什么這么干就行了:

RunLoop 中是有多個 Mode 的,而 RunLoop 需要以指定的 Mode 去運行,并且一旦運行就無法切換到其他 Mode 中。那么當你將一個 rls(run loop source) 添加到 RunLoop 的某一個 Mode 之后,一旦 RunLoop 不是運行在 rls 被添加到的 Mode 上,那么 rls 將無法被檢測并觸發(fā)到,為了解決這個問題,可以將 rls 添加到 RunLoop 中的所有 Modes 中就行了,這樣無論 RunLoop 工作在哪一個 Mode 上 rls 都有機會被檢測和觸發(fā)。

這是關于上面描述的一個具體例子:

應用場景舉例:主線程的 RunLoop 里有兩個預置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個 Mode 都已經(jīng)被標記為"Common"屬性。DefaultMode 是 App 平時所處的狀態(tài),TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態(tài)。當你創(chuàng)建一個 Timer 并加到 DefaultMode 時,Timer 會得到重復回調(diào),但此時滑動一個TableView時,RunLoop 會將 mode 切換為 TrackingRunLoopMode,這時 Timer 就不會被回調(diào),并且也不會影響到滑動操作。

那么怎么將 rls 添加到 RunLoop 所有的 Modes 中呢?于是提供了這樣的方法:

CFRunLoopAddSource(
    CFRunLoopRef rl, 
    CFRunLoopSourceRef rls, 
    CFStringRef kCFRunLoopCommonModes // 注意到 kCFRunLoopCommonModes 了嗎
); 

暫時就這么多,enjoy!

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容