博客連接重拾RunLoop原理html
雖然本身很早前就看過RunLoop的源碼,當時看得時候,有點地方仍是比較生澀的。全部抽了個時間,從新整理了一下以前RunLoop的筆記。CoreFoundation源代碼關於RunLoop的源碼主要集中在CFRunLoop.c
文件中。程序員
蘋果並不容許咱們直接建立RunLoop,RunLoop的建立在第一次獲取的時候,使用[NSRunLoop mainRunLoop]
或CFRunLoopGetMain()
能夠獲取主線程的RunLoop;經過[NSRunLoop currentRunLoop]
或CFRunLoopGetCurrent()
獲取當前線程的RunLoop。面試
它們之間的關係是Foundation
中的RunLoop是對Core Foundation
中的包裝。能夠經過執行NSLog(@"%@, %p", [NSRunLoop mainRunLoop], CFRunLoopGetMain());
得出,這裏就不貼實驗結果了。數組
接着看一下RunLoop在CFRunLoop.c
中的定義:安全
// 主線程的RunLoop
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK(); // 判斷是否須要fork進程
static CFRunLoopRef __main = NULL; // no retain needed
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}
// 當前線程的RunLoop
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
// 先從TSD中查找有沒有相關的runloop信息,有則返回。
// 咱們能夠理解爲runloop不光存在與全局字典中,也存在中TSD中。
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
複製代碼
CHECK_FOR_FORK
用來判斷是否須要fork進程,這裏咱們能夠暫時無論。bash
在獲取主線程RunLoop的時候,它使用了static CFRunLoopRef __main
進行保存,當第二次調用CFRunLoopGetMain()
,__main
是有值的,就不會再從新建立,不然就使用_CFRunLoopGet0
進行建立,傳入的是pthread_main_thread_np()
即主線程。app
在獲取當前線程的RunLoop的時候,首頁會經過_CFGetTSD
獲取RunLoop,若是沒有再經過_CFRunLoopGet0
,傳入的是當前的線程。異步
這裏介紹一下Thread-specific data
。Thread-specific data
是線程私有數據就是上面的TSD
,顧名思義就是存一些特定的數據的,RunLoop會保存在線程的私有數據裏。async
// __CFTSDTable
typedef struct __CFTSDTable {
uint32_t destructorCount;
uintptr_t data[CF_TSD_MAX_SLOTS];
tsdDestructor destructors[CF_TSD_MAX_SLOTS];
} __CFTSDTable;
// _CFGetTSD
CF_EXPORT void *_CFGetTSD(uint32_t slot) {
__CFTSDTable *table = __CFTSDGetTable();
if (!table) { return NULL; }
uintptr_t *slots = (uintptr_t *)(table->data);
return (void *)slots[slot];
}
// _CFSetTSD
CF_EXPORT void *_CFSetTSD(uint32_t slot, void *newVal, tsdDestructor destructor) {
__CFTSDTable *table = __CFTSDGetTable();
if (!table) { return NULL; }
void *oldVal = (void *)table->data[slot];
table->data[slot] = (uintptr_t)newVal;
table->destructors[slot] = destructor;
return oldVal;
}
複製代碼
__CFTSDTable
的data
數組用來保存私有數據,destructors
數組用來保存析構函數,destructorCount
用來記錄析構函數的個數。ide
_CFGetTSD
的做用就是獲取__CFTSDTable
的data
數據,並返回slot
對應的值。
_CFSetTSD
的做用就是給__CFTSDTable
裏設置data[slot]
和destructors[slot]
位置的值。
要想知道RunLoop與線程之間的關係,就須要看一下_CFRunLoopGet0
函數。
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
// 當前線程爲0,則取主線程
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
// __CFRunLoops是一個全局的靜態字典。
// 若是該字典爲空,就進行如下操做:
// 1.建立一個臨時字典;
// 2.建立主線程的RunLoop,並將它存到臨時字典裏
// 3.OSAtomicCompareAndSwapPtrBarrier用來將這個臨時字典複製到全局字典裏;
// 而且使用了鎖機制確保上述操做的安全性。
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 當前線程RunLoop的獲取,獲取不到就使用__CFRunLoopCreate建立一個RunLoop,並保存在全局字典裏
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it __CFUnlock(&loopsLock); CFRelease(newLoop); } if (pthread_equal(t, pthread_self())) { // t爲當前線程的話,將loop保存在線程私有數據中 _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL); // __CFFinalizeRunLoop是RunLoop的析構函數, // PTHREAD_DESTRUCTOR_ITERATIONS 表示是線程退出時銷燬線程私有數據的最大次數 // 這也是RunLoop的釋放時機--線程退出的時候 if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) { // 註冊一個回調,當線程銷燬時,順便也銷燬其對應的RunLoop _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); } } return loop; } 複製代碼
經過源代碼咱們能夠知道:
以前在看源碼的時候有兩個地方不是很理解:
1.爲何上面的loop要再取一次
後來在《程序員的自我修養》第29頁中獲得啓發。裏面關於單例有這樣一段代碼:
volatile T* pInst = 0;
T* GetInstance()
{
if(pInst == NULL)
{
lock();
if(pInst == NULL)
pInst = new T;
unlock();
}
return pInst;
}
複製代碼
書上只說明雙重if在這裏可讓lock的調用開銷降到最低。爲何有這個效果,這裏作一下說明。
在不考慮CPU亂序的狀況下,假設有兩個線程A、B同時訪問GetInstance()
,A和B同時執行第一個判斷語句,結果同樣,都進入了代碼塊。lock()
的設定就是隻容許一個線程進入,假設A先進入,B在等待。A進入後首先判斷pInst
爲NULL
,那麼new一個對象,而後解鎖返回對象。喚醒B,這是B進入發現第二個判斷經過不了(由於pInst
已經有值了),這樣的話B就直接解鎖返回對象。假設只有最外層的判斷的話,那麼B也會建立一個對象。
我想這裏應該也是相似的做用吧。
2.RunLoop銷燬的時機
上面的源代碼只說明瞭這個會在RunLoop的析構函數是__CFFinalizeRunLoop
,可是具體的釋放時機會在後面說明。
從_CFRunLoopGet0
函數的實現中能夠知道,RunLoop的建立是經過調用使用__CFRunLoopCreate
返回一個CFRunLoopRef
的實例,這個函數大體分爲兩步:
_CFRuntimeCreateInstance
建立一個CFRunLoopRef
實例,其實現爲CFRuntime.c
文件;CFRunLoopRef
進行初始化配置,包括調用__CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
。另外在__CFRunLoopFindMode
裏講到了RunLoop的定時器,用宏進行了判斷
#if DEPLOYMENT_TARGET_MACOSX
#define USE_DISPATCH_SOURCE_FOR_TIMERS 1
#define USE_MK_TIMER_TOO 1
#else
#define USE_DISPATCH_SOURCE_FOR_TIMERS 0
#define USE_MK_TIMER_TOO 1
#endif
複製代碼
在MACOSX
下,RunLoop會使用GCD Timer
和MK_TIMER
來作定時器,在非MACOSX
下,使用MK_TIMER
做爲定時器。
咱們知道RunLoop的釋放是發生在線程銷燬的時候。
在__CFTSDGetTable()
函數的實現中有這樣的一句代碼:
pthread_key_init_np(CF_TSD_KEY, __CFTSDFinalize);
複製代碼
經過CF_TSD_KEY
,指定了對應的析構函數__CFTSDFinalize
是一個析構函數。
__CFTSDFinalize
的實現以下:
static void __CFTSDFinalize(void *arg) {
__CFTSDSetSpecific(arg);
if (!arg || arg == CF_TSD_BAD_PTR) {
return;
}
__CFTSDTable *table = (__CFTSDTable *)arg;
table->destructorCount++;
for (int32_t i = 0; i < CF_TSD_MAX_SLOTS; i++) {
if (table->data[i] && table->destructors[i]) {
uintptr_t old = table->data[i];
table->data[i] = (uintptr_t)NULL;
table->destructors[i]((void *)(old));
}
}
if (table->destructorCount == PTHREAD_DESTRUCTOR_ITERATIONS - 1) { // On PTHREAD_DESTRUCTOR_ITERATIONS-1 call, destroy our data
free(table);
__CFTSDSetSpecific(CF_TSD_BAD_PTR);
return;
}
}
複製代碼
咱們能夠看到,table
會循環遍歷data
和destructors
的數據,而且把old
變量做爲destructors
裏函數的參數。table->destructors[i]((void *)(old));
至關於就是在調用一個析構函數。經過前面的代碼,咱們知道RunLoop的析構函數是會存到destructors
中去的。因此當線程退出的時候,會調用到RunLoop的析構函數__CFFinalizeRunLoop
釋放RunLoop。
接着看一下__CFFinalizeRunLoop
函數
// Called for each thread as it exits
CF_PRIVATE void __CFFinalizeRunLoop(uintptr_t data) {
CFRunLoopRef rl = NULL;
if (data <= 1) {
__CFLock(&loopsLock);
if (__CFRunLoops) {
rl = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(pthread_self()));
if (rl) CFRetain(rl);
// 移除全局字典中RunLoop與線程之間的映射關係
CFDictionaryRemoveValue(__CFRunLoops, pthreadPointer(pthread_self()));
}
__CFUnlock(&loopsLock);
} else {
// 遞歸移除
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(data - 1), (void (*)(void *))__CFFinalizeRunLoop);
}
if (rl && CFRunLoopGetMain() != rl) { // protect against cooperative threads
if (NULL != rl->_counterpart) {
CFRelease(rl->_counterpart);
rl->_counterpart = NULL;
}
// purge all sources before deallocation
CFArrayRef array = CFRunLoopCopyAllModes(rl);
for (CFIndex idx = CFArrayGetCount(array); idx--;) {
CFStringRef modeName = (CFStringRef)CFArrayGetValueAtIndex(array, idx);
// 移除RunLoop中的mode
__CFRunLoopRemoveAllSources(rl, modeName);
}
// 移除RunLoop中的common mode
__CFRunLoopRemoveAllSources(rl, kCFRunLoopCommonModes);
CFRelease(array);
}
if (rl) CFRelease(rl);
}
複製代碼
在CFRunLoop.c
中關於RunLoop的類一共有五個,它們分別是CFRunLoopRef
、CFRunLoopModeRef
、CFRunLoopSourceRef
、CFRunLoopObserverRef
、CFRunLoopTimerRef
。各個類之間的關係:
CFRunLoopRef
對應__CFRunLoop
結構體,它的定義以下:
struct __CFRunLoop {
// 省略其餘成員變量
...
// common mode的集合
CFMutableSetRef _commonModes;
// 每一個common mode都有的item(source,timer and observer)集合
CFMutableSetRef _commonModeItems;
// 當前runloop的mode
CFRunLoopModeRef _currentMode;
// 全部的mode的集合
CFMutableSetRef _modes;
};
複製代碼
一個RunLoop能夠包含幾個Mode,可是必須指定一個Mode來運行,它取決於_currentMode
的值。關於_currentMode
的賦值在CFRunLoopRunSpecific
函數中。
接着看CFRunLoopModeRef
,CFRunLoopModeRef
對應着__CFRunLoopMode
結構體,其定義以下:
struct __CFRunLoopMode {
CFStringRef _name;
// source0的集合
CFMutableSetRef _sources0;
// source1的集合
CFMutableSetRef _sources1;
// observer的數組
CFMutableArrayRef _observers;
// timer的數組
CFMutableArrayRef _timers;
// 省略其餘屬性
...
};
複製代碼
__CFRunLoopMode
中包含的就是RunLoop要處理的一些事情(source0/source1/observer/timer)。前面提到RunLoop必須在執行的Mode下運行,若是RunLoop須要切換Mode,只能退出Loop,再從新指定一個Mode進入。這樣的好處是:不一樣組的source0/source1/observer/timer能夠相互隔離,互不影響,從而提升執行效率。
RunLoop有五種運行模式,其中常見的一、2和5這三種
kCFRunLoopDefaultMode
:App的默認Mode,一般主線程是在這個Mode下運行;UITrackingRunLoopMode
:界面跟蹤Mode,用於滾動視圖追蹤觸摸滑動,保證界面滑動時不受其餘 Mode影響;UIInitializationRunLoopMode
:在剛啓動App時第進入的第一個Mode,啓動完成後就再也不使用,會切換到kCFRunLoopDefaultMode;GSEventReceiveRunLoopMode
:接受系統事件的內部Mode;kCFRunLoopCommonModes
:這是一個佔位用的Mode,並非一種真正的Mode;kCFRunLoopCommonModes
是蘋果提供的一種「CommonModes」。它實際上是一個標識符,並非一個具體的Mode。kCFRunLoopDefaultMode
和UITrackingRunLoopMode
,而且都被標記爲「CommonModes」。
一個Mode能夠將本身標記爲「Common」屬性(經過將其ModeName
添加到RunLoop的commonModes
中)。每當RunLoop的內容發生變化時,RunLoop都會自動將_commonModeItems
裏的source0/source1/observer/timer同步到具備「Common」標記的全部Mode裏,即能在全部具備「Common」標記的全部Mode裏運行。
以CFRunLoopAddSource
函數爲例,只關注「CommonModes」的部分:
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) {
// 該Mode是CommonMode
if (modeName == kCFRunLoopCommonModes) {
// _commonModes存在則獲取一份數據拷貝
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
if (NULL == rl->_commonModeItems) {
// _commonModeItems不存在建立一個新的集合
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
// 將source添加到_commonModeItems
CFSetAddValue(rl->_commonModeItems, rls);
if (NULL != set) {
CFTypeRef context[2] = {rl, rls};
// 調用__CFRunLoopAddItemToCommonModes函數向_commonModes中全部的Mode添加這個source
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
}
}
複製代碼
上面的source0/source1/observer/timer被統稱爲mode item
,一個item
能夠被同時加入多個Mode。若是Mode裏沒有任何source0/source1/observer/timer,RunLoop便會馬上退出。
這也解決了一個問題--爲何列表滑動的時候,NSTimer不執行回調?該如何解決?
默認NSTimer是運行在RunLoop的kCFRunLoopDefaultMode
下,在列表滑動的時候,RunLoop會切換UITrackingRunLoopMode
,由於RunLoop只能運行在一種模式下,因此NSTimer不會執行回調。 使用現成的API將NSTimer就有添加到CommonModes就能夠,kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
都已經被標爲」Common」屬性的。這樣Timer就同時加入了這兩個Mode中。
CFRunLoopSourceRef
對應着__CFRunLoopSource
結構體,其定義以下:
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
複製代碼
其中有兩個字段version0
和version1
分別對應Source0
和Source1
。
Source0
的定義以下:
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
// 當source被添加到RunLoop中後,會調用這個指針
void (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
// 當調CFRunLoopSourceInvalidate函數移除該source的時候,會調用這個指針
void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
// RunLoop處理Source0的時候,會調用這個指針
void (*perform)(void *info);
} CFRunLoopSourceContext;
複製代碼
大神的博客中提到:Source0
並不能主動觸發事件。使用時,你須要先調用CFRunLoopSourceSignal
,將這個Source標記爲待處理,而後手動調用CFRunLoopWakeUp
來喚醒RunLoop,讓其處理這個事件。
優秀的博客老是會被不少人閱讀和模仿,這是能夠理解的。可是確實沒看到有人對這幾句結論進行驗證一下,固然我一開始也是看過記住,可是並無作進一步的理解。
下面給出我本身的推導過程:
RunLoop經過__CFRunLoopDoSources0
函數處理Source0
。在它的實現有一段很關鍵的代碼:
if (__CFRunLoopSourceIsSignaled(rls)) {
__CFRunLoopSourceUnsetSignaled(rls);
if (__CFIsValid(rls)) {
__CFRunLoopSourceUnlock(rls);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(rls->_context.version0.perform, rls->_context.version0.info);
CHECK_FOR_FORK();
sourceHandled = true;
} else {
__CFRunLoopSourceUnlock(rls);
}
}
複製代碼
將其簡化一下:
if (__CFRunLoopSourceIsSignaled(rls)) {
__CFRunLoopSourceUnsetSignaled(rls);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(rls->_context.version0.perform, rls->_context.version0.info);
}
// __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__即是處理Source0的函數
// perform指針也是Source0中定義的,
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(void (*perform)(void *), void *info) {
if (perform) {
perform(info);
}
asm __volatile__(""); // thwart tail-call optimization
}
複製代碼
先判斷該Source0
是否被標記,若是是,取消該Source0
的標記,並處理。既然這樣確定存在對應的一個標記函數__CFRunLoopSourceSetSignaled
:
// CFRunLoopSourceSignal函數是對外公開的。
void CFRunLoopSourceSignal(CFRunLoopSourceRef rls) {
CHECK_FOR_FORK();
__CFRunLoopSourceLock(rls);
if (__CFIsValid(rls)) {
__CFRunLoopSourceSetSignaled(rls);
}
__CFRunLoopSourceUnlock(rls);
}
複製代碼
關於CFRunLoopSourceSignal
函數的使用,CFRunLoop.c
並無相關使用代碼。可是在CFSocket.c
文件中能找到些許痕跡。
相關代碼以下:
//
if (shared->_source) {
CFRunLoopSourceSignal(shared->_source);
_CFRunLoopSourceWakeUpRunLoops(shared->_source);
}
// CFRunLoopSourceContext表明Source0
sock->_shared->_source =
CFRunLoopSourceCreate(allocator, order, (CFRunLoopSourceContext *)&context);
if (sock->_shared->_source) {
CFRunLoopSourceSignal(sock->_shared->_source);
_CFRunLoopSourceWakeUpRunLoops(sock->_shared->_source);
}
// _CFRunLoopSourceWakeUpRunLoops是CFRunLoop.c中的內部方法
// 其核心就是調用CFRunLoopWakeUp函數
CF_PRIVATE void _CFRunLoopSourceWakeUpRunLoops(CFRunLoopSourceRef rls) {
CFBagRef loops = NULL;
__CFRunLoopSourceLock(rls);
if (__CFIsValid(rls) && NULL != rls->_runLoops) {
loops = CFBagCreateCopy(kCFAllocatorSystemDefault, rls->_runLoops);
}
__CFRunLoopSourceUnlock(rls);
if (loops) {
CFBagApplyFunction(loops, __CFRunLoopSourceWakeUpLoop, NULL);
CFRelease(loops);
}
}
static void __CFRunLoopSourceWakeUpLoop(const void *value, void *context) {
// 主動喚醒RunLoop
CFRunLoopWakeUp((CFRunLoopRef)value);
}
複製代碼
經過上面給出的相關代碼,我想能夠解釋Source0
是如何被觸發的了。
使用Source0
的狀況:
觸摸事件處理;
調用performSelector:onThread:withObject:waitUntilDone:
方法;
Source1
的定義以下:
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
mach_port_t (*getPort)(void *info);
void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
void * (*getPort)(void *info);
void (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;
複製代碼
Source1
中有一個mach_port_t
,mach_port是用於內核向線程發送消息的。 注意:Source1在處理的時候會分發一些操做給Source0去處理。
使用Source1
的狀況:
Source1
的;Source1
,接着分發到Source0
去處理這個點擊事件。CFRunLoopObserverRef
對應着__CFRunLoopObserver
結構體,實現以下:
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
複製代碼
每一個Observer
都包含了一個回調(函數指針CFRunLoopObserverCallBack _callout
),當RunLoop的狀態發生變化時,觀察者就能經過回調接受到這個變化。
RunLoop有如下幾種狀態:
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
};
複製代碼
使用Observer
的狀況:
CFRunLoopTimerRef
對應着__CFRunLoopTimer
結構體,實現以下:
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFMutableSetRef _rlModes;
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable */
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
複製代碼
使用Timer
的狀況:
CFRunLoopTimerRef
;performSelector:withObject:afterDelay:
或相似帶有afterDelay
的方法。RunLoop經過CFRunLoopRun
和CFRunLoopRunInMode
這兩個函數運行。
void CFRunLoopRun(void) {
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
複製代碼
函數默認在kCFRunLoopDefaultMode
下運行RunLoop,而且一直運行在一個do-while的循環裏。 另外函數不會主動調用CFRunLoopStop
函數(kCFRunLoopRunStopped
)或者將全部事件源移除(kCFRunLoopRunFinished
)。
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
複製代碼
不管是CFRunLoopRun
仍是CFRunLoopRunInMode
都是調用了CFRunLoopRunSpecific
。
這裏對CFRunLoopRunSpecific
函數的實現作了精簡處理:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
// 第1步:通知Observers,進入loop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 具體要作的事情
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 第10步:通知Observers,退出loop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
複製代碼
__CFRunLoopRun
能夠說是RunLoop運行的核心方法。因爲代碼過長,這裏對代碼進行了精簡:
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
// 第2步:通知Observers,即將處理Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 第3步:通知Observers,即將處理Source
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 第4步:處理Source0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
// 處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
// 第5步:判斷有無Source1,有Source1,跳轉到handle_msg
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
//
goto handle_msg;
}
didDispatchPortLastTime = false;
// 第6步:通知Observers,即將進入休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// RunLoop休眠
__CFRunLoopSetSleeping(rl);
CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
// 第7步:等待別的消息來喚醒,若是沒有被喚醒那就不會執行下面的代碼
// 這些消息多是:
// 一個基於port的Source的事件。
// 一個Timer到時間了
// RunLoop自身的超時時間到了
// 被其餘什麼調用者手動喚醒
__CFRunLoopServiceMachPort(waitSet,
&msg,
sizeof(msg_buffer),
&livePort, poll ? 0 : TIMEOUT_INFINITY,
&voucherState,
&voucherCopy);
rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));
// 取消RunLoop的休眠
__CFRunLoopUnsetSleeping(rl);
// 第8步:通知Observers,結束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 判斷RunLoop被喚醒的方式,並處理對應的事件
handle_msg:;
// 判斷RunLoop被喚醒的方式
// MACH_PORT_NULL == livePort和livePort == rl->_wakeUpPort兩種狀況什麼都不作,省略
// 被timer喚醒
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
// 處理timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
}
// 被GCD喚醒
else if (livePort == dispatchPort) {
// 處理GCD
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
// 被Source1喚醒
else {
// 處理Source1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
}
// 處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 第9步:決定RunLoop的返回值
if (sourceHandledThisLoop && stopAfterHandle) {
// 處理完事件就返回
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
// 超時
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
// RunLoop終止
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
// mode終止
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}
複製代碼
RunLoop運行流程圖:
上述過程當中有兩個地方要注意:
1.RunLoop處理GCD事件
在大多數狀況,RunLoop和GCD各自有這本身的執行流程,不會出現依賴,可是有一種狀況比較特殊。先看如下代碼:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"1111111");
});
});
複製代碼
打印函數調用棧:
使用GCD異步操做的時候,咱們在一個子線程處理完一些事情後,要返回主線程處理事情的時候,這時候須要依賴於RunLoop。內部會調用__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
函數。
2.RunLoop休眠
當RunLoop一旦休眠意味着CPU不會分配任何資源,那線程也就沒有事情幹了,也進入休眠。RunLoop休眠內部是調用了mach_msg()
函數。操做系統中有內核層面的API和應用層面的API。內核層面的API是不會輕易暴露出來,mach_msg()
能夠理解爲是應用層面的API,告訴內核休眠該線程休眠。一旦接受到系統事件,也會轉化成內核API,告訴內核須要喚醒該線程,那麼又能夠執行應用層API了。因此RunLoop的休眠能夠當作是用戶狀態到內核狀態的切換,而喚醒RunLoop就是內核狀態到用戶狀態的切換。
因爲總結的東西相對來講比較多,會以面試題的形式單獨寫一篇RunLoop面試題分析來總結
《程序員的自我修養》 CoreFoundation源代碼 深刻理解RunLoop 蘋果文檔--RunLoop