3.RunLoop

概述

RunLoop作為iOS中一個基礎(chǔ)組件和線程有著千絲萬縷的關(guān)系,同時也是很多常見技術(shù)的幕后功臣爆存。盡管在平時多數(shù)開發(fā)者很少直接使用RunLoop株扛,但是理解RunLoop可以幫助開發(fā)者更好的利用多線程編程模型,同時也可以幫助開發(fā)者解答日常開發(fā)中的一些疑惑诉探。本文將從RunLoop源碼著手日熬,結(jié)合RunLoop的實際應(yīng)用來逐步解開它的神秘面紗。

開源的RunloopRef

通常所說的RunLoop指的是NSRunloop或者CFRunloopRef肾胯,CFRunloopRef是純C的函數(shù)碍遍,而NSRunloop僅僅是CFRunloopRef的OC封裝,并未提供額外的其他功能阳液,因此下面主要分析CFRunloopRef怕敬,蘋果已經(jīng)開源了CoreFoundation源代碼,因此很容易找到CFRunloop源代碼帘皿。
從代碼可以看出CFRunloopRef其實就是__CFRunloop這個結(jié)構(gòu)體指針(按照OC的思路我們可以將RunLoop看成一個對象)东跪,這個對象的運行才是我們通常意義上說的運行循環(huán),核心方法是__CFRunloopRun()鹰溜,為了便于閱讀就不再直接貼源代碼虽填,放一段偽代碼方便大家閱讀:

 int32_t __CFRunLoopRun()
{
    // 通知即將進入runloop
    __CFRunLoopDoObservers(KCFRunLoopEntry);
    
    do
    {
        // 通知將要處理timer和source
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
        
        // 處理非延遲的主線程調(diào)用
        __CFRunLoopDoBlocks();
        // 處理Source0事件
        __CFRunLoopDoSource0();
        
        if (sourceHandledThisLoop) {
            __CFRunLoopDoBlocks();
         }
        /// 如果有 Source1 (基于port) 處于 ready 狀態(tài),直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息曹动。
        if (__Source0DidDispatchPortLastTime) {
            Boolean hasMsg = __CFRunLoopServiceMachPort();
            if (hasMsg) goto handle_msg;
        }
            
        /// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)斋日。
        if (!sourceHandledThisLoop) {
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
        }
            
        // GCD dispatch main queue
        CheckIfExistMessagesInMainDispatchQueue();
        
        // 即將進入休眠
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
        
        // 等待內(nèi)核mach_msg事件
        mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
        
        // 等待。墓陈。恶守。
        
        // 從等待中醒來
        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
        
        // 處理因timer的喚醒
        if (wakeUpPort == timerPort)
            __CFRunLoopDoTimers();
        
        // 處理異步方法喚醒,如dispatch_async
        else if (wakeUpPort == mainDispatchQueuePort)
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
            
        // 處理Source1
        else
            __CFRunLoopDoSource1();
        
        // 再次確保是否有同步的方法需要調(diào)用
        __CFRunLoopDoBlocks();
        
    } while (!stop && !timeout);
    
    // 通知即將退出runloop
    __CFRunLoopDoObservers(CFRunLoopExit);
}

一、簡介

CFRunLoopRef源碼

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

RunLoop的代碼邏輯:
詳細解釋請看這里

// 用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)鍵點在于:如何管理事件/消息,如何讓線程在沒有處理消息時休眠以避免資源占用崭闲、在有消息到來時立刻被喚醒肋联。
  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. 我們繼續(xù)深入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;
    }
}

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

3. 繼續(xù)學習CFRunLoopRef

RunLoop對象包括Fundation中的NSRunLoop對象和CoreFoundation中的CFRunLoopRef對象。因為Fundation框架是基于CoreFoundation的封裝腔稀,因此我們學習RunLoop還是要研究CFRunLoopRef 源碼盆昙。

獲取RunLoop對象

//Foundation
[NSRunLoop currentRunLoop]; // 獲得當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象
//Core Foundation
CFRunLoopGetCurrent(); // 獲得當前線程的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) {
        // 第一次進入時,初始化全局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)绣檬,當線程銷毀時渠鸽,順便也銷毀其對應(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嚼鹉。過去蘋果有份文檔標明了 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] 來獲取當前線程浑侥。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ù)的機制被稱為EventLoop巍沙,node.js 的事件處理葵姥,windows程序的消息循環(huán),iOS句携、OSX的RunLoop都是這種機制榔幸。
  1. 線程和RunLoop是一一對應(yīng)的,關(guān)系保存在全局的字典里。
    在主線程中削咆,程序啟動時牍疏,系統(tǒng)默認添加了有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  //獲得當前RunLoop和主RunLoop
CFRunLoopModeRef  //運行模式,只能選擇一種限寞,在不同模式中做不同的操作
CFRunLoopSourceRef  //事件源忍啸,輸入源
CFRunLoopTimerRef //定時器時間
CFRunLoopObserverRef //觀察者

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

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

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

CFRunLoopRef獲取Mode的接口:

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

2.CFRunLoopMode的類型

  1. kCFRunLoopDefaultMode
    App的默認Mode,通常主線程是在這個Mode下運行
  1. UITrackingRunLoopMode:
    界面跟蹤 Mode叉钥,用于 ScrollView 追蹤觸摸滑動罢缸,保證界面滑動時不受其他 Mode 影響
  2. UIInitializationRunLoopMode:
    在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用
  3. GSEventReceiveRunLoopMode:
    接受系統(tǒng)事件的內(nèi)部 Mode投队,通常用不到
  4. kCFRunLoopCommonModes:
    這是一個占位用的Mode枫疆,作為標記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 標記為待處理谎碍,然后手動調(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)聽其運行狀態(tài):

 //創(chuàng)建監(jiān)聽者
     /*
     第一個參數(shù) CFAllocatorRef allocator:分配存儲空間 CFAllocatorGetDefault()默認分配
     第二個參數(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),   //   即將進入RunLoop
     kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理Timer
     kCFRunLoopBeforeSources = (1UL << 2), // 即將處理Source
     kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠
     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進入");
                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在哪種運行模式下的狀態(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);

七恍箭、RunLoop 可能的面試套路

RunLoop面試小結(jié)

什么是RunLoop?

從字面上看:運行循環(huán)、跑圈

其實它內(nèi)部就是do-while循環(huán),在這個循環(huán)內(nèi)部不斷的處理各種任務(wù)(比如Source瞧省、Timer扯夭、Observer)

一個線程對應(yīng)一個RunLoop,主線程的RunLoop默認已經(jīng)啟動,子線程的RunLoop需要手動啟動(調(diào)用run方法)
RunLoop只能選擇一個Mode啟動,如果當前Mode中沒有任何Soure、Timer鞍匾、Observer,那么就直接退出RunLoop

在開發(fā)中如何使用RunLoop?什么應(yīng)用場景?
    開啟一個常駐線程(讓一個子線程不進入消亡狀態(tài),等待其他線程發(fā)來消息,處理其他事件)
    在子線程中開啟一個定時器
    在子線程中進行一些長期監(jiān)控

可以控制定時器在特定模式下執(zhí)行
可以讓某些事件(行為交洗、任務(wù))在特定模式下執(zhí)行
可以添加Observer監(jiān)聽RunLoop的狀態(tài),比如監(jiān)聽點擊事件的處理(在所有點擊事件之前做一些事情)

所有參考鏈接:

1、ibireme

2橡淑、百度大神

3构拳、視頻-runloop線下分享

4、RunLoop實際應(yīng)用

5梳码、官方文檔

6隐圾、CFRunLoop.c

什么是RunLoop?

RunLoop是一個接收處理異步消息事件的循環(huán),一個循環(huán)中:等待事件發(fā)生掰茶,然后將這個事件送到能處理它的地方暇藏。

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

Event Loop模型偽代碼

int main(int argc, char * argv[]) {    
     //程序一直運行狀態(tài)
     while (AppIsRunning) {
          //睡眠狀態(tài),等待喚醒事件
          id whoWakesMe = SleepForWakingU  p();
          //得到喚醒事件
          id event = GetEvent(whoWakesMe);
          //開始處理事件
          HandleEvent(event);
     }
     return 0;
}

image.png
  • mach kernel屬于蘋果內(nèi)核瓮顽,RunLoop依靠它實現(xiàn)了休眠和喚醒而避免了CPU的空轉(zhuǎn)县好。
  • Runloop是基于pthread進行管理的,pthread是基于c的跨平臺多線程操作底層API暖混。它是mach thread的上層封裝(可以參見Kernel Programming Guide)缕贡,和NSThread一一對應(yīng)(而NSThread是一套面向?qū)ο蟮腁PI,所以在iOS開發(fā)中我們也幾乎不用直接使用pthread)拣播。
image.png

RunLoop的組成

RunLoop構(gòu)成

CFRunLoop對象可以檢測某個task或者dispatch的輸入事件晾咪,當檢測到有輸入源事件,CFRunLoop將會將其加入到線程中進行處理贮配。比方說用戶輸入事件谍倦、網(wǎng)絡(luò)連接事件、周期性或者延時事件泪勒、異步的回調(diào)等昼蛀。

RunLoop可以檢測的事件類型一共有3種,分別是CFRunLoopSource圆存、CFRunLoopTimer叼旋、CFRunLoopObserver×删纾可以通過CFRunLoopAddSource, CFRunLoopAddTimer或者CFRunLoopAddObserver添加相應(yīng)的事件類型送淆。

要讓一個RunLoop跑起來還需要run loop modes,每一個source, timer和observer添加到RunLoop中時必須要與一個模式(CFRunLoopMode)相關(guān)聯(lián)才可以運行怕轿。

上面是對于CFRunLoop官方文檔的解釋

RunLoop的主要組成
RunLoop共包含5個類偷崩,但公開的只有Source、Timer撞羽、Observer相關(guān)的三個類阐斜。

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

image.png

CFRunLoopSourceRef
source是RunLoop的數(shù)據(jù)源(輸入源)的抽象類(protocol),Source有兩個版本:Source0 和 Source1

  • source0:只包含了一個回調(diào)(函數(shù)指針),使用時诀紊,你需要先調(diào)用 CFRunLoopSourceSignal(source)谒出,將這個 Source 標記為待處理,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop邻奠,讓其處理這個事件笤喳。處理App內(nèi)部事件,App自己負責管理(出發(fā)),如UIEvent(Touch事件等,GS發(fā)起到RunLoop運行再到事件回調(diào)到UI)碌宴、CFSocketRef杀狡。
  • Source1:由RunLoop和內(nèi)核管理,由mach_port驅(qū)動(特指port-based事件)贰镣,如CFMachPort呜象、CFMessagePort膳凝、NSSocketPort。特別要注意一下Mach port的概念恭陡,它是一個輕量級的進程間通訊的方式蹬音,可以理解為它是一個通訊通道,假如同時有幾個進程都掛在這個通道上休玩,那么其它進程向這個通道發(fā)送消息后著淆,這些掛在這個通道上的進程都可以收到相應(yīng)的消息。這個Port的概念非常重要哥捕,因為它是RunLoop休眠和被喚醒的關(guān)鍵牧抽,它是RunLoop與系統(tǒng)內(nèi)核進行消息通訊的窗口。

CFRunLoopTimerRef 是基于時間的觸發(fā)器遥赚,它和 NSTimer 是toll-free bridged 的,可以混用(底層基于使用mk_timer實現(xiàn))阐肤。它受RunLoop的Mode影響(GCD的定時器不受RunLoop的Mode影響)凫佛,當其加入到 RunLoop 時,RunLoop會注冊對應(yīng)的時間點孕惜,當時間點到時愧薛,RunLoop會被喚醒以執(zhí)行那個回調(diào)。如果線程阻塞或者不在這個Mode下衫画,觸發(fā)點將不會執(zhí)行毫炉,一直等到下一個周期時間點觸發(fā)。

CFRunLoopObserverRef 是觀察者削罩,每個 Observer 都包含了一個回調(diào)(函數(shù)指針)瞄勾,當 RunLoop 的狀態(tài)發(fā)生變化時,觀察者就能通過回調(diào)接受到這個變化弥激〗福可以觀測的時間點有以下幾個

enum CFRunLoopActivity {
    kCFRunLoopEntry              = (1 << 0),    // 即將進入Loop   
    kCFRunLoopBeforeTimers      = (1 << 1),    // 即將處理 Timer        
    kCFRunLoopBeforeSources     = (1 << 2),    // 即將處理 Source  
    kCFRunLoopBeforeWaiting     = (1 << 5),    // 即將進入休眠     
    kCFRunLoopAfterWaiting      = (1 << 6),    // 剛從休眠中喚醒   
    kCFRunLoopExit               = (1 << 7),    // 即將退出Loop  
    kCFRunLoopAllActivities     = 0x0FFFFFFFU  // 包含上面所有狀態(tài)  
};
typedef enum CFRunLoopActivity CFRunLoopActivity;

這里要提一句的是,timer和source1(也就是基于port的source)可以反復使用微服,比如timer設(shè)置為repeat趾疚,port可以持續(xù)接收消息,而source0在一次觸發(fā)后就會被runloop移除以蕴。

上面的 Source/Timer/Observer 被統(tǒng)稱為 mode item糙麦,一個 item 可以被同時加入多個 mode。但一個 item 被重復加入同一個 mode 時是不會有效果的丛肮。如果一個 mode 中一個 item 都沒有赡磅,則 RunLoop 會直接退出,不進入循環(huán)腾供。

RunLoop主要處理以下6類事件

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

RunLoop的Mode

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

一個RunLoop包含了多個Mode仆邓,每個Mode又包含了若干個Source/Timer/Observer鲜滩。每次調(diào)用 RunLoop的主函數(shù)時,只能指定其中一個Mode节值,這個Mode被稱作CurrentMode徙硅。如果需要切換 Mode,只能退出Loop搞疗,再重新指定一個Mode進入嗓蘑。這樣做主要是為了分隔開不同Mode中的Source/Timer/Observer,讓其互不影響匿乃。下面是5種Mode

  • kCFDefaultRunLoopMode App的默認Mode桩皿,通常主線程是在這個Mode下運行
  • UITrackingRunLoopMode 界面跟蹤Mode,用于ScrollView追蹤觸摸滑動幢炸,保證界面滑動時不受其他Mode影響
  • UIInitializationRunLoopMode 在剛啟動App時第進入的第一個Mode泄隔,啟動完成后就不再使用
  • GSEventReceiveRunLoopMode 接受系統(tǒng)事件的內(nèi)部Mode,通常用不到
  • kCFRunLoopCommonModes 這是一個占位用的Mode宛徊,不是一種真正的Mode

其中kCFDefaultRunLoopMode佛嬉、UITrackingRunLoopMode是蘋果公開的,其余的mode都是無法添加的闸天。那為何我們又可以這么用呢

[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

什么是CommonModes暖呕?

一個 Mode 可以將自己標記為”Common”屬性(通過將其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每當 RunLoop 的內(nèi)容發(fā)生變化時苞氮,RunLoop 都會自動將 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 標記的所有Mode里
主線程的 RunLoop 里有 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode湾揽,這兩個Mode都已經(jīng)被標記為”Common”屬性。當你創(chuàng)建一個Timer并加到DefaultMode時笼吟,Timer會得到重復回調(diào)库物,但此時滑動一個 scrollView 時,RunLoop 會將 mode 切換為TrackingRunLoopMode赞厕,這時Timer就不會被回調(diào)艳狐,并且也不會影響到滑動操作。
如果想讓scrollView滑動時Timer可以正常調(diào)用皿桑,一種辦法就是手動將這個 Timer 分別加入這兩個 Mode毫目。另一種方法就是將 Timer 加入到CommonMode 中。

怎么將事件加入到CommonMode诲侮?

我們調(diào)用上面的代碼將 Timer 加入到CommonMode 時镀虐,但實際并沒有 CommonMode,其實系統(tǒng)將這個 Timer 加入到頂層的 RunLoop 的 commonModeItems 中沟绪。commonModeItems 會被 RunLoop 自動更新到所有具有”Common”屬性的 Mode 里去刮便。
這一步其實是系統(tǒng)幫我們將Timer加到了kCFRunLoopDefaultMode和UITrackingRunLoopMode中。

在項目中最常用的就是設(shè)置NSTimer的Mode绽慈,比較簡單這里就不說了恨旱。

RunLoop運行機制

image.png

當你調(diào)用 CFRunLoopRun() 時辈毯,線程就會一直停留在這個循環(huán)里;直到超時或被手動停止搜贤,該函數(shù)才會返回谆沃。每次線程運行RunLoop都會自動處理之前未處理的消息,并且將消息發(fā)送給觀察者仪芒,讓事件得到執(zhí)行唁影。RunLoop運行時首先根據(jù)modeName找到對應(yīng)mode,如果mode里沒有source/timer/observer掂名,直接返回据沈。

/// 用DefaultMode啟動
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}

/// 用指定的Mode啟動,允許設(shè)置RunLoop超時時間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

/// RunLoop的實現(xiàn)
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {

    /// 首先根據(jù)modeName找到對應(yīng)mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    /// 如果mode里沒有source/timer/observer, 直接返回饺蔑。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;

    /// 1\. 通知 Observers: RunLoop 即將進入 loop锌介。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

    /// 內(nèi)部函數(shù),進入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {

        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {

            /// 2\. 通知 Observers: RunLoop 即將觸發(fā) Timer 回調(diào)膀钠。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            /// 3\. 通知 Observers: RunLoop 即將觸發(fā) Source0 (非port) 回調(diào)掏湾。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            /// 執(zhí)行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);

            /// 4\. RunLoop 觸發(fā) Source0 (非port) 回調(diào)。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            /// 執(zhí)行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);

            /// 5\. 如果有 Source1 (基于port) 處于 ready 狀態(tài)肿嘲,直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }

            /// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)筑公。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }

            /// 7\. 調(diào)用 mach_msg 等待接受 mach_port 的消息雳窟。線程將進入休眠, 直到被下面某一個事件喚醒。
            /// ? 一個基于 port 的Source 的事件匣屡。
            /// ? 一個 Timer 到時間了
            /// ? RunLoop 自身的超時時間到了
            /// ? 被其他什么調(diào)用者手動喚醒
            __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 到時間了誉结,觸發(fā)這個Timer的回調(diào)。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 

            /// 9.2 如果有dispatch到main_queue的block券躁,執(zhí)行block惩坑。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 

            /// 9.3 如果一個 Source1 (基于port) 發(fā)出事件了,處理這個事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }

            /// 執(zhí)行加入到Loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);

            if (sourceHandledThisLoop && stopAfterHandle) {
                /// 進入loop時參數(shù)說處理完事件就返回也拜。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出傳入?yún)?shù)標記的超時時間了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部調(diào)用者強制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                /// source/timer/observer一個都沒有了
                retVal = kCFRunLoopRunFinished;
            }

            /// 如果沒超時以舒,mode里沒空,loop也沒被停止慢哈,那繼續(xù)loop蔓钟。
        } while (retVal == 0);
    }

    /// 10\. 通知 Observers: RunLoop 即將退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
} 

RunLoop的掛起和喚醒

RunLoop的掛起

RunLoop的掛起是通過_CFRunLoopServiceMachPort —call—> mach_msg —call—> mach_msg_trap這個調(diào)用順序來告訴內(nèi)核RunLoop監(jiān)聽哪個mach_port(上面提到的消息通道)卵贱,然后等待事件的發(fā)生(等待與InputSource滥沫、Timer描述內(nèi)容相關(guān)的事件)侣集,這樣內(nèi)核就把RunLoop掛起了,即RunLoop休眠了兰绣。

RunLoop的喚醒

這接種情況下會被喚醒

  1. 存在Source0被標記為待處理世分,系統(tǒng)調(diào)用CFRunLoopWakeUp喚醒線程處理事件
  2. 定時器時間到了
  3. RunLoop自身的超時時間到了
  4. RunLoop外部調(diào)用者喚醒

當RunLoop被掛起后,如果之前監(jiān)聽的事件發(fā)生了狭魂,由另一個線程(或另一個進程中的某個線程)向內(nèi)核發(fā)送這個mach_port的msg后罚攀,trap狀態(tài)被喚醒,RunLoop繼續(xù)運行

處理事件

  1. 如果一個 Timer 到時間了雌澄,觸發(fā)這個Timer的回調(diào)
  2. 如果有dispatch到main_queue的block斋泄,執(zhí)行block
  3. 如果一個 Source1 發(fā)出事件了,處理這個事件

事件處理完成進行判斷

  1. 進入loop時傳入?yún)?shù)指明處理完事件就返回(stopAfterHandle)
  2. 超出傳入?yún)?shù)標記的超時時間(timeout)
  3. 被外部調(diào)用者強制停止__CFRunLoopIsStopped(runloop)
  4. source/timer/observer 全都空了__CFRunLoopModeIsEmpty(runloop, currentMode)

RunLoop 的底層實現(xiàn)

關(guān)于這個大家可以看ibireme的深入理解RunLoop一文镐牺,我這里選擇一些覺得比較重要又不是那么難懂的炫掐。
Mach消息發(fā)送機制看這篇文章Mach消息發(fā)送機制

為了實現(xiàn)消息的發(fā)送和接收,mach_msg() 函數(shù)實際上是調(diào)用了一個 Mach 陷阱 (trap)睬涧,即函數(shù)mach_msg_trap()募胃,陷阱這個概念在 Mach 中等同于系統(tǒng)調(diào)用。當你在用戶態(tài)調(diào)用 mach_msg_trap() 時會觸發(fā)陷阱機制畦浓,切換到內(nèi)核態(tài)痹束;內(nèi)核態(tài)中內(nèi)核實現(xiàn)的 mach_msg() 函數(shù)會完成實際的工作,如下圖:

image.png

RunLoop 的核心就是一個 mach_msg() (見上面代碼的第7步)讶请,RunLoop 調(diào)用這個函數(shù)去接收消息祷嘶,如果沒有別人發(fā)送 port 消息過來,內(nèi)核會將線程置于等待狀態(tài)夺溢。例如你在模擬器里跑起一個 iOS 的 App论巍,然后在 App 靜止時點擊暫停,你會看到主線程調(diào)用棧是停留在 mach_msg_trap() 這個地方风响。

RunLoop和線程

RunLoop和線程是息息相關(guān)的,我們知道線程的作用是用來執(zhí)行特定的一個或多個任務(wù),但是在默認情況下,線程執(zhí)行完之后就會退出,就不能再執(zhí)行任務(wù)了嘉汰。這時我們就需要采用一種方式來讓線程能夠處理任務(wù),并不退出。所以,我們就有了RunLoop状勤。

iOS開發(fā)中能遇到兩個線程對象: pthread_t和NSThread鞋怀,pthread_t和NSThread 是一一對應(yīng)的。比如荧降,你可以通過 pthread_main_thread_np()或 [NSThread mainThread]來獲取主線程接箫;也可以通過pthread_self()或[NSThread currentThread]來獲取當前線程。CFRunLoop 是基于 pthread 來管理的朵诫。

線程與RunLoop是一一對應(yīng)的關(guān)系(對應(yīng)關(guān)系保存在一個全局的Dictionary里)辛友,線程創(chuàng)建之后是沒有RunLoop的(主線程除外),RunLoop的創(chuàng)建是發(fā)生在第一次獲取時,銷毀則是在線程結(jié)束的時候。只能在當前線程中操作當前線程的RunLoop,而不能去操作其他線程的RunLoop废累。

蘋果不允許直接創(chuàng)建RunLoop邓梅,但是可以通過[NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent()來獲取(如果沒有就會自動創(chuàng)建一個)邑滨。


/// 全局的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) {
        // 第一次進入時匣距,初始化全局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)归榕,當線程銷毀時尸红,順便也銷毀其對應(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());
}

開發(fā)過程中需要RunLoop時刹泄,則需要手動創(chuàng)建和運行RunLoop(尤其是在子線程中, 主線程中的Main RunLoop除外)外里,我看到別人舉了這么個例子,很有意思

調(diào)用[NSTimer scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:]帶有schedule的方法簇來啟動Timer.

此方法會創(chuàng)建Timer并把Timer放到當前線程的RunLoop中特石,隨后RunLoop會在Timer設(shè)定的時間點回調(diào)Timer綁定的selector或Invocation盅蝗。但是,在主線程和子線程中調(diào)用此方法的效果是有差異的姆蘸,即在主線程中調(diào)用scheduledTimer方法時timer可以在設(shè)定的時間點觸發(fā)风科,但是在子線程里則不能觸發(fā)。這是因為子線程中沒有創(chuàng)建RunLoop且更沒有啟動RunLoop乞旦,而主線程中的RunLoop默認是創(chuàng)建好的且一直運行著。所以题山,子線程中需要像下面這樣調(diào)用兰粉。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer) userInfo:nil repeats:NO];
  [[NSRunLoop currentRunLoop] run];
});

那為什么下面這樣調(diào)用同樣不會觸發(fā)Timer呢?
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  [[NSRunLoop currentRunLoop] run];
  [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer) userInfo:nil repeats:NO];
});  

我的分析是:scheduledTimerWithTimeInterval內(nèi)部在向RunLoop傳遞Timer時是調(diào)用與線程實例相關(guān)的單例方法[NSRunLoop currentRunLoop]來獲取RunLoop實例的顶瞳,即RunLoop實例不存在就創(chuàng)建一個與當前線程相關(guān)的RunLoop并把Timer傳遞到RunLoop中玖姑,存在則直接傳Timer到RunLoop中即可。而在RunLoop開始運行后再向其傳遞Timer時慨菱,由于dispatch_async代碼塊里的兩行代碼是順序執(zhí)行焰络,[[NSRunLoop currentRunLoop] run]是一個沒有結(jié)束時間的RunLoop,無法執(zhí)行到“[NSTimer scheduledTimerWithTimeInterval:…”這一行代碼符喝,Timer也就沒有被加到當前RunLoop中闪彼,所以更不會觸發(fā)Timer了斧拍。

蘋果用 RunLoop 實現(xiàn)的功能

AutoreleasePool

App啟動之后惯殊,系統(tǒng)啟動主線程并創(chuàng)建了RunLoop,在main thread中注冊了兩個observer,回調(diào)都是_wrapRunLoopWithAutoreleasePoolHandler()

第一個Observer監(jiān)視的事件

  1. 即將進入Loop(kCFRunLoopEntry)村象,其回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動釋放池。其order是-2147483647营勤,優(yōu)先級最高旁舰,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。

第二個Observer監(jiān)視了兩個事件

  1. 準備進入休眠(kCFRunLoopBeforeWaiting)铭污,此時調(diào)用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 來釋放舊的池并創(chuàng)建新的池恋日。

  2. 即將退出Loop(kCFRunLoopExit)此時調(diào)用 _objc_autoreleasePoolPop()釋放自動釋放池。這個 Observer的order是2147483647嘹狞,確保池子釋放在所有回調(diào)之后岂膳。

我們知道AutoRelease對象是被AutoReleasePool管理的,那么AutoRelease對象在什么時候被回收呢刁绒?

第一種情況:在我們自己寫的for循環(huán)或線程體里闷营,我們都習慣用AutoReleasePool來管理一些臨時變量的autorelease,使得在for循環(huán)或線程結(jié)束后回收AutoReleasePool的時候來回收AutoRelease臨時變量知市。

另一種情況:我們在主線程里創(chuàng)建了一些AutoRelease對象傻盟,這些對象可不能指望在回收Main AutoReleasePool時才被回收,因為App一直運行的過程中Main AutoReleasePool是不會被回收的嫂丙。那么這種AutoRelease對象的回收就依賴Main RunLoop的運行狀態(tài)娘赴,Main RunLoop的Observer會在Main RunLoop結(jié)束休眠被喚醒時(kCFRunLoopAfterWaiting狀態(tài))通知UIKit,UIKit收到這一通知后就會調(diào)用_CFAutorleasePoolPop方法來回收主線程中的所有AutoRelease對象跟啤。

在主線程中執(zhí)行代碼一般都是寫在事件回調(diào)或Timer回調(diào)中的诽表,這些回調(diào)都被加入了main thread的自動釋放池中,所以在ARC模式下我們不用關(guān)心對象什么時候釋放隅肥,也不用去創(chuàng)建和管理pool竿奏。(如果事件不在主線程中要注意創(chuàng)建自動釋放池,否則可能會出現(xiàn)內(nèi)存泄漏)腥放。

NSTimer(timer觸發(fā))

上文說到了CFRunLoopTimerRef泛啸,其實NSTimer的原型就是CFRunLoopTimerRef。一個Timer注冊 RunLoop 之后秃症,RunLoop 會為這個Timer的重復時間點注冊好事件候址。有兩點需要注意:

  1. 但是需要注意的是RunLoop為了節(jié)省資源,并不會在非常準確的時間點回調(diào)這個Timer种柑。Timer 有個屬性叫做 Tolerance (寬容度)岗仑,標示了當時間點到后,容許有多少最大誤差聚请。這個誤差默認為0荠雕,我們可以手動設(shè)置這個誤差。文檔最后還強調(diào)了,為了防止時間點偏移舞虱,系統(tǒng)有權(quán)力給這個屬性設(shè)置一個值無論你設(shè)置的值是多少欢际,即使RunLoop 模式正確,當前線程并不阻塞矾兜,系統(tǒng)依然可能會在 NSTimer 上加上很小的的容差损趋。
  2. 我們在哪個線程調(diào)用 NSTimer 就必須在哪個線程終止

在RunLoop的Mode中也有說到,NSTimer使用的時候注意Mode,比如我之前開發(fā)時候用NSTimer寫一個Banner圖片輪播框架椅寺,如果不設(shè)置Timer的Mode為commonModes那么在滑動TableView的時候Banner就停止輪播

DispatchQueue.global().async {
    // 非主線程不能使用 Timer.scheduledTimer進行初始化
//                    self.timer = Timer.scheduledTimer(timeInterval: 6.0, target: self, selector: #selector(TurnPlayerView.didTurnPlay), userInfo: nil, repeats: false)

    if #available(iOS 10.0, *) {
        self.timer = Timer(timeInterval: 6.0, repeats: true, block: { (timer) in
            self.setContentOffset(CGPoint(x: self.frame.width*2, y: self.contentOffset.y), animated: true)
        })
    } else {
        // Fallback on earlier versions
    }

    RunLoop.main.add(self.timer!, forMode: RunLoopMode.commonModes)
}

和GCD的關(guān)系

  1. RunLoop底層用到GCD
  2. RunLoop與GCD并沒有直接關(guān)系浑槽,但當GCD使用到main_queue時才有關(guān)系,如下:
//實驗GCD Timer 與 Runloop的關(guān)系返帕,只有當dispatch_get_main_queue時才與RunLoop有關(guān)系
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"GCD Timer...");
});

當調(diào)用 dispatch_async(dispatch_get_main_queue(), block) 時桐玻,libDispatch 會向主線程的 RunLoop 發(fā)送消息,RunLoop會被喚醒荆萤,并從消息中取得這個 block镊靴,并在回調(diào) CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里執(zhí)行這個 block。但這個邏輯僅限于 dispatch 到主線程链韭,dispatch 到其他線程仍然是由 libDispatch 處理的偏竟。同理,GCD的dispatch_after在dispatch到main_queue時的timer機制才與RunLoop相關(guān)敞峭。

PerformSelecter

NSObject的performSelecter:afterDelay: 實際上其內(nèi)部會創(chuàng)建一個 Timer 并添加到當前線程的 RunLoop 中踊谋。所以如果當前線程沒有 RunLoop,則這個方法會失效旋讹。
NSObject的performSelector:onThread: 實際上其會創(chuàng)建一個 Timer 加到對應(yīng)的線程去殖蚕,同樣的,如果對應(yīng)線程沒有 RunLoop 該方法也會失效沉迹。
其實這種方式有種說法也叫創(chuàng)建常駐線程(內(nèi)存)睦疫,AFNetworking也用到這種技法。舉個例子鞭呕,如果把RunLoop去掉笼痛,那么test方法就不會執(zhí)行。


class SecondViewController: UIViewController {

    var thread: Thread!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.red
        thread = Thread.init(target: self, selector: #selector(SecondViewController.run), object: nil)
        thread.start()

    }

    @objc func run() {

        print("run -- ")
        RunLoop.current.add(Port(), forMode: .defaultRunLoopMode)
        RunLoop.current.run()
    }

    @objc func test() {
        print("test --  \(Thread.current)")
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        //        self.test()

        self.perform(#selector(SecondViewController.test), on: thread, with: nil, waitUntilDone: false)

    }

}

網(wǎng)絡(luò)請求

iOS中的網(wǎng)絡(luò)請求接口自下而上有這么幾層

image.png

其中CFSocket和CFNetwork偏底層琅拌,早些時候比較知名的網(wǎng)絡(luò)框架AFNetworking是基于NSURLConnection編寫的,iOS7之后新增了NSURLSession摘刑,NSURLSession的底層仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 線程)进宝,之后AFNetworking和Alamofire就是基于它封裝的了。

image.png

通常使用 NSURLConnection 時枷恕,會傳入一個 Delegate党晋,當調(diào)用了 [connection start] 后,這個 Delegate 就會不停收到事件回調(diào)。實際上未玻,start 這個函數(shù)的內(nèi)部會獲取 CurrentRunLoop灾而,然后在其中的 DefaultMode 添加了4個 Source0 (即需要手動觸發(fā)的Source)。CFMultiplexerSource 是負責各種 Delegate 回調(diào)的扳剿,CFHTTPCookieStorage 是處理各種 Cookie 的旁趟。

開始網(wǎng)絡(luò)傳輸時,NSURLConnection 創(chuàng)建了兩個新線程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private庇绽。

其中 CFSocket 線程是處理底層 socket 連接的锡搜,NSURLConnectionLoader中的RunLoop通過一些基于mach port的Source1接收來自底層CFSocket的通知。當收到通知后瞧掺,其會在合適的時機向CFMultiplexerSource等Source0發(fā)送通知耕餐,同時喚醒Delegate線程的RunLoop來讓其處理這些通知。CFMultiplexerSource會在Delegate線程的RunLoop對Delegate執(zhí)行實際的回調(diào)辟狈。

事件響應(yīng)

蘋果注冊了一個 Source1 (基于 mach port 的) 用來接收系統(tǒng)事件肠缔,其回調(diào)函數(shù)為 __IOHIDEventSystemClientQueueCallback()。

當一個硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后哼转,首先由 IOKit.framework 生成一個 IOHIDEvent 事件并由 SpringBoard 接收明未。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸释簿,加速亚隅,接近傳感器等幾種 Event,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進程庶溶。

觸摸事件其實是Source1接收系統(tǒng)事件后在回調(diào) __IOHIDEventSystemClientQueueCallback()內(nèi)觸發(fā)的 Source0煮纵,Source0 再觸發(fā)的 _UIApplicationHandleEventQueue()。source0一定是要喚醒runloop及時響應(yīng)并執(zhí)行的偏螺,如果runloop此時在休眠等待系統(tǒng)的 mach_msg事件行疏,那么就會通過source1來喚醒runloop執(zhí)行。

_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理并包裝成 UIEvent 進行處理或分發(fā)套像,其中包括識別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow 等酿联。

image.png

手勢識別

當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調(diào)用 Cancel 將當前的 touchesBegin/Move/End 系列回調(diào)打斷夺巩。隨后系統(tǒng)將對應(yīng)的 UIGestureRecognizer 標記為待處理贞让。

蘋果注冊了一個 Observer 監(jiān)測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver()柳譬,其內(nèi)部會獲取所有剛被標記為待處理的 GestureRecognizer喳张,并執(zhí)行GestureRecognizer的回調(diào)。

當有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時美澳,這個回調(diào)都會進行相應(yīng)處理销部。

UI更新

Core Animation 在 RunLoop 中注冊了一個 Observer 監(jiān)聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件 摸航。當在操作 UI 時,比如改變了 Frame舅桩、更新了 UIView/CALayer 的層次時酱虎,或者手動調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個 UIView/CALayer 就被標記為待處理擂涛,并被提交到一個全局的容器去读串。當Oberver監(jiān)聽的事件到來時,回調(diào)執(zhí)行函數(shù)中會遍歷所有待處理的UIView/CAlayer 以執(zhí)行實際的繪制和調(diào)整歼指,并更新 UI 界面爹土。

如果此處有動畫,通過 DisplayLink 穩(wěn)定的刷新機制會不斷的喚醒runloop踩身,使得不斷的有機會觸發(fā)observer回調(diào)胀茵,從而根據(jù)時間來不斷更新這個動畫的屬性值并繪制出來。

函數(shù)內(nèi)部的調(diào)用棧

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                          [CALayer layoutSublayers];
                          [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                          [CALayer display];
                          [UIView drawRect];

繪圖和動畫有兩種處理的方式:CPU(中央處理器)和GPU(圖形處理器)
CPU: CPU 中計算顯示內(nèi)容挟阻,比如視圖的創(chuàng)建琼娘、布局計算、圖片解碼附鸽、文本繪制等
GPU: GPU 進行變換脱拼、合成、渲染.

關(guān)于CADisplayLink的描述有兩種

CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現(xiàn)原理更復雜坷备,和 NSTimer 并不一樣熄浓,其內(nèi)部實際是操作了一個 Source)。如果在兩次屏幕刷新之間執(zhí)行了一個長任務(wù)省撑,那其中就會有一幀被跳過去(和 NSTimer 相似)赌蔑,造成界面卡頓的感覺。在快速滑動TableView時竟秫,即使一幀的卡頓也會讓用戶有所察覺娃惯。

CADisplayLink是一個執(zhí)行頻率(fps)和屏幕刷新相同(可以修改preferredFramesPerSecond改變刷新頻率)的定時器,它也需要加入到RunLoop才能執(zhí)行肥败。與NSTimer類似趾浅,CADisplayLink同樣是基于CFRunloopTimerRef實現(xiàn),底層使用mk_timer(可以比較加入到RunLoop前后RunLoop中timer的變化)馒稍。和NSTimer相比它精度更高(盡管NSTimer也可以修改精度)皿哨,不過和NStimer類似的是如果遇到大任務(wù)它仍然存在丟幀現(xiàn)象。通常情況下CADisaplayLink用于構(gòu)建幀動畫纽谒,看起來相對更加流暢往史,而NSTimer則有更廣泛的用處。

不管怎么樣CADisplayLink和NSTimer是有很大不同的佛舱,詳情可以參考這篇文章CADisplayLink

ibireme根據(jù)CADisplayLink的特性寫了個FPS指示器YYFPSLabel椎例,代碼非常少
原理是這樣的:既然CADisplayLink可以以屏幕刷新的頻率調(diào)用指定selector,而且iOS系統(tǒng)中正常的屏幕刷新率為60Hz(60次每秒)请祖,所以使用 CADisplayLink 的 timestamp 屬性订歪,配合 timer 的執(zhí)行次數(shù)計算得出FPS數(shù)

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadTest) object:nil];
    [_thread start];
}

- (void)threadTest
{
    [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(TimerTest) userInfo:nil repeats:YES];
    while (1) {
        [[NSRunLoop currentRunLoop] run];
        NSLog(@"++++++++");
    }
}

- (void)TimerTest
{
    NSLog(@"----%@-----", [NSThread currentThread]);
}

打印線程,不打印 NSLog(@"++++++++");

只在滾動的時候監(jiān)聽定制器啟動

    NSTimer*timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(find) userInfo:nil repeats:YES];// 在UITrackingRunLoopMode模式下定時器才會運行
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

參考文章
深入理解RunLoop
iOS 事件處理機制與圖像渲染過程
RunLoop學習筆記(一) 基本原理介紹
iOS刨根問底-深入理解RunLoop
【iOS程序啟動與運轉(zhuǎn)】- RunLoop個人小結(jié)
RunLoop的前世今生
Runloop知識樹
RunLoop入門 看我就夠了
RunLoop已入門肆捕?不來應(yīng)用一下刷晋?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市慎陵,隨后出現(xiàn)的幾起案子眼虱,更是在濱河造成了極大的恐慌,老刑警劉巖席纽,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捏悬,死亡現(xiàn)場離奇詭異,居然都是意外死亡润梯,警方通過查閱死者的電腦和手機过牙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來纺铭,“玉大人寇钉,你說我怎么就攤上這事〔芭猓” “怎么了扫倡?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長竟纳。 經(jīng)常有香客問我撵溃,道長,這世上最難降的妖魔是什么蚁袭? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任征懈,我火速辦了婚禮,結(jié)果婚禮上揩悄,老公的妹妹穿的比我還像新娘卖哎。我一直安慰自己,他們只是感情好删性,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布亏娜。 她就那樣靜靜地躺著,像睡著了一般蹬挺。 火紅的嫁衣襯著肌膚如雪维贺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天巴帮,我揣著相機與錄音溯泣,去河邊找鬼虐秋。 笑死,一個胖子當著我的面吹牛垃沦,可吹牛的內(nèi)容都是我干的客给。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼肢簿,長吁一口氣:“原來是場噩夢啊……” “哼靶剑!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起池充,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤桩引,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后收夸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體坑匠,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年咱圆,在試婚紗的時候發(fā)現(xiàn)自己被綠了笛辟。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡序苏,死狀恐怖手幢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情忱详,我是刑警寧澤围来,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站匈睁,受9級特大地震影響监透,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜航唆,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一胀蛮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧糯钙,春花似錦粪狼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至享潜,卻和暖如春困鸥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背剑按。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工疾就, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留澜术,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓猬腰,卻偏偏與公主長得像瘪板,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子漆诽,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 今天突然想到的兩句話一個叫做吃青蛙一個叫做堅持、不要臉锣枝、堅持不要臉 吃青蛙是指自己非常不想做的事情厢拭,每天早上從吃青...
    黑土錢閱讀 181評論 0 3
  • 今天早上有點忙 終于解決了weekly report 下午領(lǐng)了臺筆記本 學了個vlookup blake真是一個特...
    角落蜷縮閱讀 88評論 0 0
  • 1. 下載SDK 1.1 選擇對應(yīng)的SDK 1.2 手動下載 1.3 獲取秘鑰, 使用BundleId創(chuàng)建秘鑰A...
    GC風暴閱讀 524評論 0 0
  • 2017年8月7日,這算是正式開始上班的日子撇叁」回想從7月17日在段報到以來,我們頂著炎炎烈日進行一系列入路...
    管宜楓閱讀 763評論 0 4