首發於 我的博客html
runloop 是什麼?Runloop 仍是比較顧名思義的一個東西,說白了就是一種循環,只不過它這種循環比較高級。通常的 while 循環會致使 CPU 進入忙等待狀態,而 Runloop 則是一種「閒」等待,這部分能夠類比 Linux 下的 epoll。當沒有事件時,Runloop 會進入休眠狀態,有事件發生時, Runloop 會去找對應的 Handler 處理事件。Runloop 可讓線程在須要作事的時候忙起來,不須要的話就讓線程休眠git
回答問題以前,咱們先看源碼github
RunLoop 源碼 opensource.apple.com/tarballs/CF… 裏面數字最大的是最 新的,下載最新的 CF-1153.18.tar.gz(寫本文時候的最新版本)面試
查看源碼 中的bash
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
複製代碼
經過app
_CFRunLoopGet0 獲取的
複製代碼
進去查看作了什麼oop
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
複製代碼
發現有這麼一個獲取線程的方法,也就是傳入一個線程做爲key,獲取一個loop,若是loop爲空,就以這個線程爲key建立runloop源碼分析
小結:ui
接下來認識一下runloop的主要類 Core Foundation中關於RunLoop的5個類spa
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
複製代碼
看一下runloop結構體
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;
};
複製代碼
只保留主要的就剩下了
typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode; //當前模式
CFMutableSetRef _modes; //全部的模式
CFMutableSetRef _modes;
};
複製代碼
理解爲CFRunLoopRef中包含有_modes,modes是由 CFRunLoopModeRef組成的集合 這些modes中,只有一種是當前模式,稱爲 _currentMode
接下來咱們看看runloopmode中究竟有什麼,一樣,只保留主要的,關鍵就是下面4個
總結起來就是
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};
複製代碼
咱們能夠理解爲,RunLoop中有許多模式,但當前運行的只有一種,一個圖來表示,就是
具體在某一種runloop中的運行邏輯,官方給出下圖
那麼,前面說的,_sources0、_sources一、_observers、_timers J究竟包含了什麼呢? 先用一張圖來總結一下,而後再詳細介紹
如上圖所示,_sources0 包含觸摸事件,和 performSelector:onThread: 跑一下代碼證實一下, 首先建立一個新項目,實現點擊事件,NSLog只是爲了打斷點
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"這個打印只是爲了打斷點");
}
@end
斷點暫停以後,輸入lldb指令 bt 以後如圖所示
複製代碼
從打印日誌來看 調用了__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ 這個SOURCE0方法 那麼接下來驗證一下performSelector
由上圖可知,performSelector 也是執行了source0那咱們再看一下Timer
如上圖所示,此次是__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__對於其餘幾種狀況,讀者課自行驗證。
用一幅圖來總結RunLoop的運行邏輯
要想分析源碼首先要知道入口在哪裏,由前面的斷點可知
入口爲 CFRunLoopRunSpecific 去源碼中找到以後發現有不少。只保留關鍵信息SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
// 通知Observers: 進入Loop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 具體要作的事情
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知Observers: 退出Loop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
複製代碼
那咱們繼續跟__CFRunLoopRun 看看作了什麼,發現裏面很長的東西,整理了一下,只保留關鍵代碼,以下
/* rl, rlm are locked on entrance and exit */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
// 通知Observers: 即將處理Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知Observers: 即將處理Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 處理Sources0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
// 處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
//判斷有無source1
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) { // 若是有source1 就跳轉到 handle_msg
goto handle_msg;
}
// 通知Observers: 即將休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
// 通知Observers: 結束休眠
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
handle_msg:;
if (被Timer喚醒) {
// 處理Timers
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
} else if (被gcd喚醒) {
//處理GCD
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { //能來到這裏,就說明被Source1喚醒
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
// 處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 設置返回值,決定是否繼續循環
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
voucher_mach_msg_revert(voucherState);
os_release(voucherCopy);
} while (0 == retVal);
return retVal;
}
複製代碼
截圖下來的話,就是這樣的
###調用細節 前面說了大概的流程,那麼,具體怎麼調用的呢,lldb調試堆棧的時候,那些方法怎麼調用的呢?這裏以 __CFRunLoopDoTimers 爲例,看下源碼怎麼調用的
上圖可知,關鍵代碼是 CFRunLoopTimerRef 繼續查看 CFRunLoopTimerRef 關鍵代碼是__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ 看到這裏是否是對前面截圖中的調用堆棧更清晰了呢。 其餘幾種也都是相似的邏輯,就不贅述了。目前已知的Mode有5種
kCFRunLoopDefaultMode:App的默認Mode,一般主線程是在這個Mode下運行
UITrackingRunLoopMode:界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響
UIInitializationRunLoopMode:在剛啓動 App 時第進入的第一個 Mode,啓動完成後就再也不使用
GSEventReceiveRunLoopMode:接受系統事件的內部 Mode,一般用不到
kCFRunLoopCommonModes:這是一個佔位用的Mode,不是一種真正的Mode
複製代碼
/* Run Loop Observer Activities */
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
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製代碼
常見的2種Mode
kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默認Mode,一般主線程是在這個Mode下運行
UITrackingRunLoopMode:界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響
咱們知道,默認狀況下,NSTimer計時器,會被UIScrollView 打斷,會影響計時器的使用。緣由就是滾動時候,RunLoop切換到了UITrackingRunLoopMode模式下,但計時器在NSDefaultRunLoopMode下,因此就中止了。解決辦法就是設置NSRunLoopCommonModes。特別注意的是:
NSRunLoopCommonModes並非一個真的模式,它只是一個標記
本文參考資料:
更多資料,歡迎關注我的公衆號