RunLoop 淺析

RunLoop 淺析

一個(gè)小應(yīng)用

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

那么首先想到的就是使用兩個(gè)線程,一個(gè) Main 一個(gè) Worker,在 Main 中響應(yīng)用戶的操作,而將實(shí)際的耗時(shí)任務(wù)放到 Worker 中。

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

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

#import <Foundation/Foundation.h>

// 『消息隊(duì)列(messages queue)』這個(gè)名詞想必是家喻戶曉了
// 這里 commands 就相當(dāng)于一個(gè)消息隊(duì)列的作用
// 主線程在收到了用戶的 command 之后并不是
// 立即處理它們,轉(zhuǎn)而將其添加到這個(gè) queue 中,
// 然后 Worker 會(huì)逐個(gè)的處理這個(gè)命令
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 中我們
    // 采用了『輪詢』的方式,就是不斷的
    // 詢問消息隊(duì)列,是不是有新消息來了
    while (1) {
        NSString* last = popCommand();
        // 如果通過不斷的輪詢得到新的命令
        // 那么就處理那個(gè)命令
        while (last) {
            NSLog(@"[Worker] executing command: %@", last);
            sleep(2); // 模擬耗時(shí)的計(jì)算所需的時(shí)間
            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ù)響應(yīng)
            NSLog(@"[Main] added new command: %@", cmd);
        } while (c != 'q');
    }
    return 0;
}

運(yùn)行下這個(gè)程序,然后切換到 Debug navigator,會(huì)看到這樣的結(jié)果:

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

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

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

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

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

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

RunLoop

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

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

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

// Main 除了需要標(biāo)記相關(guān)的 run loop source 是 ready-to-be-fired 之外,
// 還需要調(diào)用 CFRunLoopWakeUp 來喚醒指定的 RunLoop
// RunLoop 是不能手動(dòng)創(chuàng)建的,所以必須注冊這個(gè)回調(diào)來向 Main 暴露 Worker
// 的 RunLoop,這樣在 Main 中才知道要喚醒誰
static CFRunLoopRef workerRunLoop = nil;
// 這也是一個(gè) run loop source 相關(guān)的回調(diào),它發(fā)生在 run loop source 被添加到
// run loop 時(shí),通過注冊這個(gè)回調(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 會(huì)通過 rls 和 Worker 協(xié)調(diào)工作
    CFRunLoopAddSource(CFRunLoopGetCurrent(), _rlSource, kCFRunLoopDefaultMode);
    // 線程需要手動(dòng)運(yùn)行 RunLoop
    CFRunLoopRun();
    NSLog(@"[Worker] is stopping...");
}

@end

// 告訴 Worker 任務(wù)來了
// 把 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 相關(guān)的選項(xiàng)以及回調(diào)
        // 另外我們這的第一個(gè)參數(shù)是 0,必須是 0
        // 這樣創(chuàng)建的 run loop source 就被添加在
        // run loop 中的 _sources0,作為用戶創(chuàng)建的
        // 非自動(dò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;
}

可以運(yùn)行一下看下性能如何:

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

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

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

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

這都是嘛

有這么幾個(gè)名詞真是非常的饒人:RunLoop、RunLoop SourceRunLoop ModeCommonMode ...

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

首先是看下 RunLoop 的結(jié)構(gòu):

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

于是看到,與 RunLoop 有直接關(guān)系的是 RunLoop Mode。那么看看 RunLoop Mode 的結(jié)構(gòu):

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

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

于是就有了這個(gè)圖:

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

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

void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName);

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

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

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

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

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

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

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

這是關(guān)于上面描述的一個(gè)具體例子:

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

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

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

暫時(shí)就這么多,enjoy!

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

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

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