iOS RunLoop詳解

一、簡介

CFRunLoopRef源碼

RunLoop是一個對象,這個對象在循環(huán)中用來處理程序運(yùn)行過程中出現(xiàn)的各種事件(比如說觸摸事件、UI刷新事件、定時器事件、Selector事件),從而保持程序的持續(xù)運(yùn)行;而且在沒有事件處理的時候,會進(jìn)入睡眠模式,從而節(jié)省CPU資源,提高程序性能。

RunLoop的代碼邏輯:
詳細(xì)解釋請看這里

// 用DefaultMode啟動
void CFRunLoopRun(void) {    /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

  1. 這種模型通常被稱作 Event Loop。 Event Loop 在很多系統(tǒng)和框架里都有實現(xiàn),比如 Node.js 的事件處理,比如 Windows 程序的消息循環(huán),再比如 OSX/iOS 里的 RunLoop。實現(xiàn)這種模型的關(guān)鍵點(diǎn)在于:如何管理事件/消息,如何讓線程在沒有處理消息時休眠以避免資源占用、在有消息到來時立刻被喚醒。
  1. RunLoop管理了其需要處理的事件和消息,并提供了一個入口函數(shù)來執(zhí)行上面 Event Loop 的邏輯。線程執(zhí)行了這個函數(shù)后,就會一直處于這個函數(shù)內(nèi)部 "接受消息->等待->處理" 的循環(huán)中,直到這個循環(huán)結(jié)束(比如傳入 quit 的消息),函數(shù)返回。

二、RunLoop的深入分析

1. 從程序入口main函數(shù)開始

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

程序主線程一開始,就會一直跑,那么猜想其內(nèi)部一定是開啟了一個和主線程對應(yīng)的RunLoop
并且可以看出函數(shù)返回的是一個int返回值的 UIApplicationMain()函數(shù)

2. UIApplicationMain函數(shù)

UIKIT_EXTERN int UIApplicationMain
(int argc, 
char *argv[], 
NSString * __nullable principalClassName,
 NSString * __nullable delegateClassName
);

我們發(fā)現(xiàn)它返回的是一個int類型的值,那么我們對main函數(shù)做一些修改:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSLog(@"開始");
        int re = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        NSLog(@"結(jié)束");
        return re;
    }
}

運(yùn)行程序,我們發(fā)現(xiàn)只會打印開始,并不會打印結(jié)束,這再次說明在UIApplicationMain函數(shù)中,開啟了一個和主線程相關(guān)的RunLoop,導(dǎo)致UIApplicationMain不會返回,一直在運(yùn)行中,也就保證了程序的持續(xù)運(yùn)行。

3. 繼續(xù)學(xué)習(xí)CFRunLoopRef

RunLoop對象包括Fundation中的NSRunLoop對象和CoreFoundation中的CFRunLoopRef對象。因為Fundation框架是基于CoreFoundation的封裝,因此我們學(xué)習(xí)RunLoop還是要研究CFRunLoopRef 源碼。

獲取RunLoop對象

//Foundation
[NSRunLoop currentRunLoop]; // 獲得當(dāng)前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象
//Core Foundation
CFRunLoopGetCurrent(); // 獲得當(dāng)前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象

1. 主線程獲取CFRunLoopRef源碼

 // 創(chuàng)建字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 創(chuàng)建主線程 根據(jù)傳入的主線程創(chuàng)建主線程對應(yīng)的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主線程 將主線程-key和RunLoop-Value保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);

2. 創(chuàng)建與子線程相關(guān)聯(lián)的CFRunLoopRe源碼
蘋果不允許直接創(chuàng)建 RunLoop,它只提供了兩個自動獲取的函數(shù):CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。

// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
// 獲取一個 pthread 對應(yīng)的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    if (!loopsDic) {
        // 第一次進(jìn)入時,初始化全局Dic,并先為主線程創(chuàng)建一個 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    // 直接從 Dictionary 里獲取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    if (!loop) {
        // 取不到時,創(chuàng)建一個
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        // 注冊一個回調(diào),當(dāng)線程銷毀時,順便也銷毀其對應(yīng)的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

3. CFRunloopRef與線程之間的關(guān)系

  1. 首先,iOS 開發(fā)中能遇到兩個線程對象: pthread_t 和 NSThread。過去蘋果有份文檔標(biāo)明了 NSThread 只是 pthread_t 的封裝,但那份文檔已經(jīng)失效了,現(xiàn)在它們也有可能都是直接包裝自最底層的 mach thread。蘋果并沒有提供這兩個對象相互轉(zhuǎn)換的接口,但不管怎么樣,可以肯定的是 pthread_t 和 NSThread 是一一對應(yīng)的。比如,你可以通過 pthread_main_np() 或 [NSThread mainThread] 來獲取主線程;也可以通過 pthread_self() 或 [NSThread currentThread] 來獲取當(dāng)前線程。CFRunLoop 是基于 pthread 來管理的。
  1. CFRunLoopRef源碼可以看出,線程和 RunLoop 之間是一一對應(yīng)的,其關(guān)系是保存在一個全局的 Dictionary 里。線程剛創(chuàng)建時并沒有 RunLoop,如果你不主動獲取,那它一直都不會有。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時,RunLoop 的銷毀是發(fā)生在線程結(jié)束時。你只能在一個線程的內(nèi)部獲取其 RunLoop(主線程除外)。
    [NSRunLoop currentRunLoop];方法調(diào)用時,會先看一下字典里有沒有存子線程相對用的RunLoop,如果有則直接返回RunLoop,如果沒有則會創(chuàng)建一個,并將與之對應(yīng)的子線程存入字典中。

. 總結(jié)來說. CFRunloopRef與線程之間的關(guān)系

  1. 線程在處理完自己的任務(wù)后一般會退出,為了實現(xiàn)線程不退出能夠隨時處理任務(wù)的機(jī)制被稱為EventLoop,node.js 的事件處理,windows程序的消息循環(huán),iOS、OSX的RunLoop都是這種機(jī)制。
  1. 線程和RunLoop是一一對應(yīng)的,關(guān)系保存在全局的字典里。
    在主線程中,程序啟動時,系統(tǒng)默認(rèn)添加了有kCFRunLoopDefaultMode 和 UITrackingRunLoopMode兩個預(yù)置Mode的RunLoop,保證程序處于等待狀態(tài),如果接收到來自觸摸事件等,就會執(zhí)行任務(wù),否則處于休眠中。
  2. 線程創(chuàng)建時并沒有RunLoop,(主線程除外),RunLoop不能創(chuàng)建,只能主動獲取才會有。RunLoop的創(chuàng)建是在第一次獲取時,RunLoop的銷毀是發(fā)生在線程結(jié)束時。只能在一個線程中獲取自己和主線程的RunLoop。

Core Foundation中關(guān)于RunLoop的5個類

CFRunLoopRef  //獲得當(dāng)前RunLoop和主RunLoop
CFRunLoopModeRef  //運(yùn)行模式,只能選擇一種,在不同模式中做不同的操作
CFRunLoopSourceRef  //事件源,輸入源
CFRunLoopTimerRef //定時器時間
CFRunLoopObserverRef //觀察者

** CFRunLoopModeRef**
詳細(xì)內(nèi)容請點(diǎn)擊這里
1.簡介:

每個CFRunLoopRef 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer。每次調(diào)用 RunLoop 的主函數(shù)時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode。如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進(jìn)入。這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響。

CFRunLoopModeRef 類并沒有對外暴露,只是通過 CFRunLoopRef 的接口進(jìn)行了封裝。他們的關(guān)系如下:

CFRunLoopRef獲取Mode的接口:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);

2.CFRunLoopMode的類型

  1. kCFRunLoopDefaultMode
    App的默認(rèn)Mode,通常主線程是在這個Mode下運(yùn)行
  1. UITrackingRunLoopMode:
    界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
  2. UIInitializationRunLoopMode:
    在剛啟動 App 時第進(jìn)入的第一個 Mode,啟動完成后就不再使用
  3. GSEventReceiveRunLoopMode:
    接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到
  4. kCFRunLoopCommonModes:
    這是一個占位用的Mode,作為標(biāo)記kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一種真正的Mode

3.CFRunLoopMode 和 CFRunLoop 的結(jié)構(gòu)

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 
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set 
    ...
};

CFRunLoopSourceRef

  1. 是事件產(chǎn)生的地方。Source有兩個版本:Source0 和 Source1。
  1. Source0 只包含了一個回調(diào)(函數(shù)指針),它并不能主動觸發(fā)事件。使用時,你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個 Source 標(biāo)記為待處理,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。
  2. Source1 包含了一個 mach_port 和一個回調(diào)(函數(shù)指針),被用于通過內(nèi)核和其他線程相互發(fā)送消息。這種 Source 能主動喚醒 RunLoop 的線程.

** CFRunLoopObserverRef**

CFRunLoopObserverRef是觀察者,能夠監(jiān)聽RunLoop的狀態(tài)改變。

我們直接來看代碼,給RunLoop添加監(jiān)聽者,監(jiān)聽其運(yùn)行狀態(tài):

 //創(chuàng)建監(jiān)聽者
     /*
     第一個參數(shù) CFAllocatorRef allocator:分配存儲空間 CFAllocatorGetDefault()默認(rèn)分配
     第二個參數(shù) CFOptionFlags activities:要監(jiān)聽的狀態(tài) kCFRunLoopAllActivities 監(jiān)聽所有狀態(tài)
     第三個參數(shù) Boolean repeats:YES:持續(xù)監(jiān)聽 NO:不持續(xù)
     第四個參數(shù) CFIndex order:優(yōu)先級,一般填0即可
     第五個參數(shù) :回調(diào) 兩個參數(shù)observer:監(jiān)聽者 activity:監(jiān)聽的事件
     */
     /*
     所有事件
     typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
     kCFRunLoopEntry = (1UL << 0),   //   即將進(jìn)入RunLoop
     kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理Timer
     kCFRunLoopBeforeSources = (1UL << 2), // 即將處理Source
     kCFRunLoopBeforeWaiting = (1UL << 5), //即將進(jìn)入休眠
     kCFRunLoopAfterWaiting = (1UL << 6),// 剛從休眠中喚醒
     kCFRunLoopExit = (1UL << 7),// 即將退出RunLoop
     kCFRunLoopAllActivities = 0x0FFFFFFFU
     };
     */
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"RunLoop進(jìn)入");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"RunLoop要處理Timers了");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"RunLoop要處理Sources了");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"RunLoop要休息了");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"RunLoop醒來了");
                break;
            case kCFRunLoopExit:
                NSLog(@"RunLoop退出了");
                break;

            default:
                break;
        }
    });

    // 給RunLoop添加監(jiān)聽者
    /*
     第一個參數(shù) CFRunLoopRef rl:要監(jiān)聽哪個RunLoop,這里監(jiān)聽的是主線程的RunLoop
     第二個參數(shù) CFRunLoopObserverRef observer 監(jiān)聽者
     第三個參數(shù) CFStringRef mode 要監(jiān)聽RunLoop在哪種運(yùn)行模式下的狀態(tài)
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
     /*
     CF的內(nèi)存管理(Core Foundation)
     凡是帶有Create、Copy、Retain等字眼的函數(shù),創(chuàng)建出來的對象,都需要在最后做一次release
     GCD本來在iOS6.0之前也是需要我們釋放的,6.0之后GCD已經(jīng)納入到了ARC中,所以我們不需要管了
     */
    CFRelease(observer);
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

  • 參考資料:ibireme :http://blog.ibireme.com/2015/05/18/runloop/...
    代碼移動工程師閱讀 306評論 0 0
  • 1 Runloop機(jī)制原理 深入理解RunLoop http://www.cocoachina.com/ios/2...
    Kevin_Junbaozi閱讀 4,239評論 4 30
  • 一、概述 一般來說,一個線程只能執(zhí)行一個任務(wù),執(zhí)行完就會退出,如果我們需要一種機(jī)制,讓線程能隨時處理時間但并不退出...
    一直在路上66閱讀 343評論 0 0
  • ios RunLoop詳解 一、概述 一般來說,一個線程只能執(zhí)行一個任務(wù),執(zhí)行完就會退出。如果我們需要一種機(jī)制,讓...
    721e472431a4閱讀 1,022評論 0 1
  • 一.RunLoop介紹 1.概念 RunLoop是一個運(yùn)行循環(huán),正是因為RunLoop,IOS才可以保持程序的持續(xù)...
    一片姜汁閱讀 500評論 0 0

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