iOS底層原理(三):RunLoop

一、什么是RunLoop?
    1. RunLoop就是運行循環(huán),在程序運行過程中循環(huán)做一些事情,在很多地方都會應(yīng)用到,例如:定時器、PerformSelector、GCD Async Main Queue、事件響應(yīng)、手勢識別、界面刷新、網(wǎng)絡(luò)請求、AutoreleasePool
    1. 如果沒有RunLoop的話,例如以下代碼,在執(zhí)行完第13行代碼后,會即將退出程序
沒有RunLoop的話
    1. 如果有了RunLoop的話,例如以下代碼,程序并不會馬上退出,而是保持運行狀態(tài)
有了RunLoop
    1. 我們見到的程序長時間保持活躍狀態(tài)都是RunLoop的功勞,UIApplicationMain()會創(chuàng)建主線程,主線程內(nèi)部會主動開啟一個RunLoop,而RunLoop本質(zhì)上就是一個do-while循環(huán),只要條件滿足,就會不停的循環(huán),進而程序一直保持運行的狀態(tài)。RunLoop源碼如下所示:
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. RunLoop的作用有:
    • 保持程序處于持續(xù)運行

    • 處理App中的各種事件,例如觸摸事件、定時器事件

    • 節(jié)省CPU資源、提高程序性能:該做事的時候做事

二、RunLoop對象
    1. iOS中有兩套API來訪問和使用RunLoop:Foundation中的NSRunLoop、Core Foundation中的CFRunLoopRef,其中的NSRunLoopCFRunLoopRef都代表著RunLoop對象,NSRunLoop是基于CFRunLoopRef的一層OC包裝
    1. CFRunLoop是開源的,地址是:https://opensource.apple.com/tarballs/CF/,CFRunLoopRef提供了兩個自動獲取RunLoop的函數(shù):CFRunLoopGetMain()、CFRunLoopGetCurrent(),其內(nèi)部邏輯如下:
/// 全局的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.從上述源代碼可以看出,RunLoop與線程之間的關(guān)系如下:

    • 每條線程都有唯一的一個與之對應(yīng)的RunLoop對象

    • RunLoop保存在一個全局的Dictionary里,線程作為Key,RunLoop作為Value

    • 線程剛創(chuàng)建時并沒有RunLoop對象,RunLoop會在第一次獲取它時創(chuàng)建

    • RunLoop會在線程結(jié)束時銷毀

    • 主線程的RunLoop默認已經(jīng)自動創(chuàng)建了,而子線程默認沒有開啟RunLoop

    1. 獲取RunLoop對象的方法:
    • Foundation框架:
[NSRunLoop currentRunLoop]; // 獲得當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象
  • Core Foundation框架:
CFRunLoopGetCurrent(); // 獲得當前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象
三、RunLoop對外的接口
    1. 在 CoreFoundation 里面關(guān)于 RunLoop 有5個類: CFRunLoopRef、CFRunLoopModeRef、CFRunLoopSourceRef、CFRunLoopTimerRef、CFRunLoopObserverRef,它們之間的關(guān)系如下所示:
      RunLoop
    1. 從上圖,我們可以看出來,一個RunLoop里面可以有多個mode,每個mode又可以多個source,observer,timer。可是每次RunLoop只能指定一個mode運行,如果想要切換mode,就必須先退出RunLoop,然后重新指定mode運行,這樣做的目的就是避免mode之間相互影響

    注意: 如果Mode里沒有任何Souce0、Souce1、Timer、Observer,RunLoop會立馬退出

下面我們就詳細說說關(guān)于RunLoop的五個類

    1. CFRunLoopRef是一個叫做__CFRunLoop的結(jié)構(gòu)體,其內(nèi)部結(jié)構(gòu)如下所示,內(nèi)部存放著可用的Mode集合、線程、當前的運行Mode
      __CFRunLoop內(nèi)部結(jié)構(gòu)
    1. CFRunLoopModeRef代表了RunLoop的運行模式,底層是__CFRunLoopMode的結(jié)構(gòu)體,內(nèi)部結(jié)構(gòu)如下所示,內(nèi)部存放著souce0、souce1、observers、timers
      __CFRunLoopMode結(jié)構(gòu)體.png

創(chuàng)建RunLoop時,系統(tǒng)默認注冊了五種mode:

- (1). ```kCFRunLoopDefaultMode```: 默認 mode,通常主線程在這個 mode 下運行

- (2). ```UITrackingRunLoopMode```: 追蹤mode,保證Scrollview滑動順暢不受其他 mode 影響

- (3). ```UIInitializationRunLoopMode```: 啟動程序后的過渡mode,啟動完成后就不再使用

- (4). ```GSEventReceiveRunLoopMode```: Graphic相關(guān)事件的mode,通常用不到

- (5). ```kCFRunLoopCommonModes```: 占位用的mode,作為標記kCFRunLoopDefaultMode和UITrackingRunLoopMode用
    1. CFRunLoopSourceRef是事件產(chǎn)生的地方,分為Source0Source1兩種:
    • Source0: 只包含了一個回調(diào)(函數(shù)指針),它并不能主動觸發(fā)事件。使用時,你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然后手動調(diào)用CFRunLoopWakeUp(runloop)來喚醒 RunLoop,讓其處理這個事件

    • Source1: 包含了一個 mach_port 和一個回調(diào)(函數(shù)指針),被用于通過內(nèi)核和其他線程相互發(fā)送消息。這種 Source 能主動喚醒 RunLoop 的線程

    1. CFRunLoopTimerRef是基于時間的觸發(fā)器,它和 NSTimertoll-free bridged 的,可以混用。其包含一個時間長度和一個回調(diào)(函數(shù)指針)。當其加入到 RunLoop 時,RunLoop會注冊對應(yīng)的時間點,當時間點到時,RunLoop會被喚醒以執(zhí)行那個回調(diào)
    1. CFRunLoopObserverRef是觀察者,每個 Observer 都包含了一個回調(diào)(函數(shù)指針),當 RunLoop 的狀態(tài)發(fā)生變化時,觀察者就能通過回調(diào)接受到這個變化。可以觀測的時間點有以下幾個:
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
};
    1. 可以通過該添加Observer監(jiān)聽RunLoop的狀態(tài),如下所示:
image.png
四、RunLoop的運行邏輯
    1. RunLoop的運行邏輯如下所示:
image.png
    1. 上圖的運行邏輯文字描述是這樣的:
image.png
    1. RunLoop 的核心就是一個 mach_msg(),當一個RunLoop處理完事件后,即將進入休眠時,會經(jīng)歷下面幾步:
    • (1). 指定一個將來喚醒自己的mach_port端口

    • (2). 調(diào)用mach_msg來監(jiān)聽這個端口,保持mach_msg_trap狀態(tài)

    • (3). 由另一個線程(比如有可能有一個專門處理鍵盤輸入事件的loop在后臺一直運行)向內(nèi)核發(fā)送這個端口的msg后,mach_msg_trap狀態(tài)被喚醒,RunLoop繼續(xù)運行

五、RunLoop的應(yīng)用
    1. 線程包活,例如:AFNetworking的老版本就使用了線程包活,讓子線程永遠的活著,其源碼如下所示:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
          [[NSThread currentThread] setName:@"AFNetworking"];
          NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
          [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
          [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}
    • 主線程的RunLoop會自動創(chuàng)建,子線程的RunLoop默認不創(chuàng)建,在子線程中調(diào)用NSRunLoop.current獲取RunLoop對象的時候,就會創(chuàng)建RunLoop
    • RunLoop 啟動前內(nèi)部必須要有至少一個 Timer/Source,所以 AFNetworking[runLoop run] 之前先創(chuàng)建了一個新的 NSMachPort 添加進去了。通常情況下,調(diào)用者需要持有這個 NSMachPort (mach_port) 并在外部線程通過這個 port 發(fā)送消息到 loop 內(nèi);但此處添加 port 只是為了讓 RunLoop 不至于退出,并沒有用于實際的發(fā)送消息。
    1. 事件響應(yīng),當一個事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由 SpringBoard 接收,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進程,然后觸發(fā)進程的Source1的_UIApplicationHandleEventQueue()回調(diào),_UIApplicationHandleEventQueue()會把 IOHIDEvent處理并包裝成 UIEvent 進行處理或分發(fā),其中包括識別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow 等,通常事件比如 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調(diào)中完成的。
    • 當我們點擊某個按鈕時,通過打斷點,可以看出來函數(shù)調(diào)用棧是通過Source0過來的 ,如下所示,與我們上面描述的似乎有些不同,這是因為:事件確實是由Source1接受的,在其回調(diào)里會觸發(fā)Souce0,Source0 再觸發(fā)的 _UIApplicationHandleEventQueue()進行事件分發(fā)和處理,所以UIButton事件看到是在 Source0 內(nèi)的。
image.png
    1. 界面刷新,當在操作 UI 時,比如改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個 UIView/CALayer 就被標記為待處理,并被提交到一個全局的容器去。
    • 蘋果注冊了一個 Observer 監(jiān)聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,回調(diào)去執(zhí)行一個很長的函數(shù):_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv(),這個函數(shù)里會遍歷所有待處理的 UIView/CALayer 以執(zhí)行實際的繪制和調(diào)整,并更新 UI 界面。
六、面試題
    1. 講講 RunLoop,項目中有用到嗎?
    1. runloop內(nèi)部實現(xiàn)邏輯?
    1. runloop和線程的關(guān)系?
    1. timer 與 runloop 的關(guān)系?
    1. 程序中添加每3秒響應(yīng)一次的NSTimer,當拖動tableview時timer可能無法響應(yīng)要怎么解決?
    1. runloop 是怎么響應(yīng)用戶操作的, 具體流程是什么樣的?
    1. 說說runLoop的幾種狀態(tài)
    1. runloop的mode作用是什么?
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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