iOS RunLoop解析

RunLoop

RunLoop概念

RunLoop理解為運行循環(huán)。其本質就是一個do-while,這里的do-while和普通的do-while循環(huán)不一樣,一般的 while 循環(huán)會導致 CPU 進入忙等待狀態(tài),而 Runloop 則是一種“閑”等待,當沒有事件時,Runloop 會進入休眠狀態(tài),有事件發(fā)生時, Runloop 會去找對應的 Handler 處理事件。Runloop 可以讓線程在需要做事的時候忙起來,不需要的話就讓線程休眠。

image.png

圖中展現(xiàn)了 Runloop 在線程中的作用:從 input source 和 timer source 接受事件,然后在線程中處理事件。

RunLoop 作用

  1. 保持程序持續(xù)運行。程序啟動就會自動開啟一個runloop在主線程
  2. 處理App中的各種事件
  3. 節(jié)省CPU資源,提高程序性能 有事情則做事,沒事情則休眠

RunLoop 和線程之間的關系

  • Runloop 和線程是綁定在一起的。每個線程(包括主線程)都有一個對應的 Runloop 對象。我們并不能自己創(chuàng)建 Runloop 對象,但是可以獲取到系統(tǒng)提供的 Runloop 對象。
  • 主線程的 Runloop 會在應用啟動的時候完成啟動,其他線程的 Runloop 默認并不會啟動,需要我們手動啟動。

底層RunLoop的存儲使用的是字典結構,線程是key,對應的runloop是value。

  1. 主線程是默認開啟RunLoop的
  2. 子線程是默認不開啟RunLoop的
  3. 子線程開啟RunLoop需要我們手動開啟,手動開啟時會先在全局的存儲字典里根據(jù)傳入的線程key,查看value是否存在,不存在的話就基于線程為key創(chuàng)建一個新的runloop并存儲進字典里。

RunLoop Mode

RunLoop Mode可以理解為RunLoop的運行模式,在蘋果文檔里定義了有五種運行模式。

- kCFRunLoopDefaultMode, App的默認運行模式,通常主線程是在這個運行模式下運行
- UITrackingRunLoopMode, 跟蹤用戶交互事件(用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他Mode影響)
- kCFRunLoopCommonModes, 偽模式,不是一種真正的運行模式
- UIInitializationRunLoopMode:在剛啟動App時第進入的第一個Mode,啟動完成后就不再使用
- GSEventReceiveRunLoopMode:接受系統(tǒng)內(nèi)部事件,通常用不到

在源碼里搜索__CFRunLoopMode可以看到里面的內(nèi)部結構,提取出關鍵的成員變量。

  1. _name:Mode的名字
  2. _sources0:
    1. App內(nèi)部事件,由App自己管理的,像UIEvent、CFSocket、以及performSelector不帶afterDelay參數(shù)的方法都是source0
    2. source0不能主動觸發(fā),需要調(diào)用CFRunLoopWakeUp(runloop) 來喚醒 RunLoop
  3. _sources1:
    1. 由RunLoop和內(nèi)核管理,Mach port驅動,如CFMachPort、CFMessagePort。
    2. source1包含了一個 mach_port 和一個回調(diào)(函數(shù)指針),被用于通過內(nèi)核和其他線程相互發(fā)送消息
    3. 能主動喚醒 RunLoop 的線程
  4. _observers:添加的觀察者
  5. _timers:定時器,包括NSTimer、CADisplayLink以及performSelector帶afterDelay參數(shù)的方法。
struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    __CFPortSet _portSet;
}

RunLoop的本質

RunLoop底層其實就是一個結構體,通過源碼搜索__CFRunLoop。
提取出關鍵的成員變量_pthread、_currentMode以及_modes,可以發(fā)現(xiàn)

  1. RunLoop和線程是綁定的,每個線程會有對應的RunLop,只是主線程是默認開啟的,子線程不是默認開啟的,需要手動開啟,然后底層就會根據(jù)傳入的線程創(chuàng)建新的RunLoop
  2. _currentMode:當前運行的Mode
  3. _modes:多個mode的集合
struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;          /* locked for accessing mode list */
    __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

通過上面的整理可以發(fā)現(xiàn)

  1. RunLoop管理著多個mode
  2. 每次RunLoop運行的的時候都必須指定一個mode作為_currentMode,如果需要執(zhí)行其他mode的事務,那么就要先退出當前mode,然后切換另一個mode
  3. mode里面管理著source0、source1、timers、observers以及port端口的事務。

引用來自掘金博主的圖片??


image.png

RunLoop的核心方法

通過查找源碼,我們可以發(fā)現(xiàn)三個關鍵的方法

//通知觀察者即將進入RunLoop
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    //進入RunLoop的關鍵方法
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //通知觀察者即將退出RunLoop
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

__CFRunLoopRun

通過下面的源代碼解析,可以將RunLoop的處理邏輯理為

  1. 通知觀察者,即將進入runloop
  2. 通知觀察者,即將處理Timers
  3. 通知觀察者,即將處理Sources
  4. 執(zhí)行加入到runloop的blocks
  5. 處理Source0,處理之后可能會再次處理blocks
  6. 如果存在Source1,直接跳轉到第9步
  7. 通知觀察者,即將進入休眠
  8. 通知觀察者,結束休眠
  9. 執(zhí)行喚醒RunLoop的事件
    1. 處理timer事件
    2. 執(zhí)行gcd通過異步函數(shù)提交任務到主線程的Block
    3. 處理Source1事件
    4. 被手動喚醒,不需要執(zhí)行事件,單純起到喚醒RunLoop的功能
  10. 執(zhí)行加入到runloop的blocks
  11. 根據(jù)以下情況來確定是否要退出當前RunLoop
    1. 進入run方法時參數(shù)表明處理完事件就返回。
    2. 超出run方法參數(shù)中的超時時間
    3. 被外部調(diào)用者強制停止了
    4. 調(diào)用_CFRunLoopStopMode將mode停止了
    5. 檢測還有沒有待處理的sources/timer/observer
    6. 以上??五種情況均無的話 那么就跳轉到第2步,繼續(xù)循環(huán)
  12. 通知觀察者,退出RunLoop

以下代碼引用掘金博主的源碼解析


do {
// 消息緩沖區(qū),用戶緩存內(nèi)核發(fā)的消息
uint8_t msg_buffer[3 * 1024]; 
//取所有需要監(jiān)聽的port
__CFPortSet waitSet = rlm->_portSet;
//設置RunLoop為可以被喚醒狀態(tài)
__CFRunLoopUnsetIgnoreWakeUps(rl);

//1.通知 Observers: RunLoop 即將處理 Timer 回調(diào)。
if (rlm->_observerMask & kCFRunLoopBeforeTimers) 
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);

//2。通知 Observers: RunLoop 即將觸發(fā) Source(非port) 回調(diào)
 if (rlm->_observerMask & kCFRunLoopBeforeSources) 
 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

// 執(zhí)行被加入的block
//外部通過調(diào)用CFRunLoopPerformBlock函數(shù)向當前runloop增加block。新增加的block保存咋runloop.blocks_head鏈表里。
//__CFRunLoopDoBlocks會遍歷鏈表取出每一個block,如果block被指定執(zhí)行的mode和當前的mode一致,則調(diào)用__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__執(zhí)行
 __CFRunLoopDoBlocks(rl, rlm);

//RunLoop 觸發(fā) Source0 (非port) 回調(diào)
// __CFRunLoopDoSources0函數(shù)內(nèi)部會調(diào)用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__函數(shù)
//__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__函數(shù)會調(diào)用source0的perform回調(diào)函數(shù),即rls->context.version0.perform
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);

//處理了source0后再次處理blocks
if (sourceHandledThisLoop) {
     __CFRunLoopDoBlocks(rl, rlm);
 }
//
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
didDispatchPortLastTime = false;

//如果有 Source1 (基于port) 處于 ready 狀態(tài),直接處理這個 Source1 然后跳轉去處理消息。
msg = (mach_msg_header_t *)msg_buffer;
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
     goto handle_msg;
}

//通知oberver即將進入休眠狀態(tài)
if(!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);

//接收waitSet端口的消息
//等待接受 mach_port 的消息。線程將進入休眠
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

 // 計算線程沉睡的時長
 rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));
 
 __CFPortSetRemove(dispatchPort, waitSet);
 __CFRunLoopSetIgnoreWakeUps(rl);
  // runloop置為喚醒狀態(tài)
 __CFRunLoopUnsetSleeping(rl);
  // 8. 通知 Observers: RunLoop對應的線程剛被喚醒。
 if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

//收到處理的消息進行處理
handle_msg:;
// 忽略端口喚醒runloop,避免在處理source1時通過其他線程或進程喚醒runloop(保證線程安全)
__CFRunLoopSetIgnoreWakeUps(rl);

if(MACH_PORT_NULL == livePort){
// livePort為null則什么也不做

}else if(livePort == rl->_wakeUpPort){
// livePort為wakeUpPort則只需要簡單的喚醒runloop(rl->_wakeUpPort是專門用來喚醒runloop的)
 CFRUNLOOP_WAKEUP_FOR_WAKEUP();

}else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort){
    //如果是一個timerPort
// 如果一個 Timer 到時間了,觸發(fā)這個Timer的回調(diào)
// __CFRunLoopDoTimers返回值代表是否處理了這個timer
 if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
     __CFArmNextTimerInMode(rlm, rl);
 }
}else if(livePort == dispatchPort){
   //如果是GCD port
   //處理GCD通過port提交到主線程的事件
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}else{
    // 處理source1事件(觸發(fā)source1的回調(diào))
    //// runloop 觸發(fā)source1的回調(diào),__CFRunLoopDoSource1內(nèi)部會調(diào)用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
   __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
}


/// 執(zhí)行加入到Loop的block
 __CFRunLoopDoBlocks(rl, rlm);
        
if (sourceHandledThisLoop && stopAfterHandle) {
     /// 進入loop時參數(shù)說處理完事件就返回。
     retVal = kCFRunLoopRunHandledSource; // 4
} else if (timeout_context->termTSR < mach_absolute_time()) {
            /// 超出傳入?yún)?shù)標記的超時時間了
            retVal = kCFRunLoopRunTimedOut; // 3
        } else if (__CFRunLoopIsStopped(rl)) {
            /// 被外部調(diào)用者強制停止了
            __CFRunLoopUnsetStopped(rl); // 2
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            // 調(diào)用了_CFRunLoopStopMode將mode停止了
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped; // 2
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            // source0/source1/timers/blocks一個都沒有了
            retVal = kCFRunLoopRunFinished; // 1
        }
}while(0 == retVal)
__CFRunLoopDoBlocks

解析關于runloop調(diào)用的blocks,而Source0、Source1、timers以及Observers 在前面已經(jīng)解析過了。
這個方法主要處理Blocks回調(diào)。

  • runloop對象有一個_block_item結構的鏈表,里面存儲著當前runloop待處理的blocks
  • 執(zhí)行__CFRunLoopDoBlocks方法就是遍歷runloop的鏈表,取出blocks,然后執(zhí)行__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);方法
  • KVO回調(diào)方法,以及在主線程調(diào)用的block;這兩種情況回調(diào)由__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);方法調(diào)用執(zhí)行
CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE(msg);
  • 在調(diào)用GCD的異步函數(shù)dispatch_async,往主線程里添加任務時會觸發(fā)該方法
  • 由于子線程默認是沒有開啟Runloop的,子線程下執(zhí)行GCD的任務是不會被添加到RunLoop上的,子線程blocks的執(zhí)行是由lidbdispatch驅動完成的。

RunLoop的應用

AutoreleasePool

App啟動后,蘋果在主線RunLoop里注冊了兩個觀察者,分別觀察RunLoop的即將進入loop事件、即將進入休眠事件、即將退出事件。

  • 即將進入loop事件觸發(fā)時會調(diào)用_objc_autoreleasePoolPush()方法創(chuàng)建自動釋放池以及插入哨兵對象
  • 即將進入休眠事件觸發(fā)時會分別調(diào)用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()方法,釋放舊的自動釋放池并release里面的對象以及創(chuàng)建新的自動釋放池
  • 即將退出事件觸發(fā)時會調(diào)用_objc_autoreleasePoolPop() 方法,釋放自動釋放池。

NSTimer

NSTimer就是上文提到的timer

  • 底層Runloop會在每個時間點都注冊一個事件,到了事件點進行回調(diào),但是并不是每次都是很準確地在指定時間點進行回調(diào)的,timer中有一個屬性Tolerance標識可以容忍多大的誤差。
  • 如果RunLoop有很多要處理的事,錯過了timer指定的時間點,那么是會錯過此次回調(diào)的。
  • 默認在主線創(chuàng)建的timer都會自動加入到RunLoop中,而如果是在子線程中創(chuàng)建的timer,在沒有開啟RunLoop的情況下,其實是無效的。

CADisplayLink

  • CADisplayLink是一個執(zhí)行頻率(fps)和屏幕刷新相同的定時器,需要加入到RunLoop才能執(zhí)行。但是我們也可以通過調(diào)用API來實現(xiàn)更改執(zhí)行頻率。
  • 它與NSTimer都是定時器,區(qū)別在于CADisplayLink的精度更高,而NSTimer的使用范圍更加地廣泛。

事件響應

  • 蘋果注冊了一個 Source1 (基于 mach port 的) 用來接收系統(tǒng)事件,其回調(diào)函數(shù)為 __IOHIDEventSystemClientQueueCallback()。
  • 當一個硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由 IOKit.framework 生成一個 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨后用 mach port 轉發(fā)給需要的App進程。隨后蘋果注冊的那個 Source1 就會觸發(fā)回調(diào),并調(diào)用 _UIApplicationHandleEventQueue()進行應用內(nèi)部的分發(fā)。
  • _UIApplicationHandleEventQueue() 會把 IOHIDEvent處理并包裝成 UIEvent 進行處理或分發(fā),其中包括識別 UIGesture/處理屏幕旋轉/發(fā)送給 UIWindow 等。通常事件比如 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調(diào)中完成的。

手勢識別

  • 當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調(diào)用Cancel 將當前的 touchesBegin/Move/End 系列回調(diào)打斷。隨后系統(tǒ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)都會進行相應處理。

UI更新

當修改了View的frame等導致View的圖層發(fā)生了變化或者手動調(diào)用了setNeedsDisplay/setNeedsLayout方法之后就會將這個View標記放進一個全局容器里面,當RunLoop的即將進入休眠或者退出事件回調(diào)時,就會遍歷這個全局容器將UI進行繪制更新。

PerformSelector 的實現(xiàn)原理

  • 當調(diào)用 NSObject 的 performSelecter:afterDelay: 后,實際上其內(nèi)部會創(chuàng)建一個 Timer 并添加到當前線程的 RunLoop 中。所以如果當前線程沒有 RunLoop,則這個方法會失效。
  • 當調(diào)用 performSelector:onThread: 時,觸發(fā)的是Source0事件,同樣的,如果對應線程沒有 RunLoop 該方法也會失效。
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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

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