iOS RunLoop完全指南
什麼是RunLoop
概念
什麼是RunLoop,顧名思義,RunLoop就是在‘跑圈’,其本質是一個do
while迴圈。RunLoop提供了這麼一種機制,當有任務處理時,執行緒的RunLoop會保持忙碌,而在沒有任何任務處理時,會讓執行緒休眠,從而讓出CPU。當再次有任務需要處理時,RunLoop會被喚醒,來處理事件,直到任務處理完畢,再次進入休眠。
上面這段話,相信絕多數人都知道,大致知道RunLoop是個什麼玩意。
如果繼續問到:RunLoop是怎麼實現休眠機制的?RunLoop都可以處理哪些任務,又是怎麼處理的呢?RunLoop在iOS系統中都有什麼應用呢?
可能很多人都很茫然。這是公司在篩選面試者時常用的手段,考察面試者對問題理解的深度,這從側面反映了一個人的學習能力以及未來的可塑性,這也是為什麼許多大公司偏愛問原理性問題的原因。
當然,瞭解了底層實現,對我們平日寫程式碼的優化,主動避免一些坑,也是很有幫助的。
現在,就讓我們一起深入瞭解RunLoop吧~
從main函式說起
先看一下下面這個命令列程式:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog (@"Hello, World!");
}
return 0;
}
如果我們執行一下程式的話,會發現在命令列輸出“Hello, World!”後,程式的程序自動退出:
原因很簡單,因為我們的主執行緒完成了程式碼邏輯,return 0,程式結束。
再來看一下我們平常所寫的iOS APP的main程式入口(main.m):
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
程式碼和命令列版本的main函式很像,在main裡面,直接return 了UIApplicationMain函式的返回值。
但是奇怪的是,我們的iOS APP卻不會像命令列程式一樣退出,這是為什麼?
其實,UIKit會在UIApplicationMain函式中,啟動main runloop,使得main runloop一直在跑圈(沒事兒時在睡覺),這樣,UIApplicationMain函式就不會立刻返回,我們的APP也就不會退出啦~
如果打系統斷點CFRunLoopRunInMode的話,會看到在UIApplicationMain函式中,系統啟動了main runloop:
RunLoop的結構組成
RunLoop位於蘋果的Core Foundation庫中,而Core Foundation庫則位於iOS架構分層的Core Service層中(值得注意的是,Core Foundation是一個跨平臺的通用庫,不僅支援Mac,iOS,同時也支援Windows):
在CF中,和RunLoop相關的結構有下面幾個類:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
RunLoop的組成結構如下圖:
你可能需要不時回過頭來看這張圖,來加深理解RunLoop的結構以及他們之間的相互作用關係。
當我們在自己的APP中下斷點時,有90%的機率會看到呼叫堆疊中有如下六個函式之一,它們都是RunLoop不同情境下的回撥函式:
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
這也說明了我們的RunLoop都能幹些什麼事情,而且系統的大部分功能,都是和RunLoop相關的。GCD並不是基於RunLoop的(除了dispatch 到main queue),所以,你在GCD的呼叫堆疊中,是看不到它們的。
RunLoop對應的資料結構為:
// Foundation:
NSRunLoop : NSObject
// Core Foundation:
struct __CFRunLoop
__CFRunLoop * CFRunLoopRef
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;
};
CFRunLoopRef 與 NSRunLoop之間的轉換時toll-free的。關於RunLoop的具體實現程式碼,我們會在下面提到。
RunLoop提供瞭如下功能(括號中CF**表明了在CF庫中對應的資料結構名稱):
- RunLoop(CFRunLoop)使你的執行緒保持忙碌(有事幹時)或休眠狀態(沒事幹時)間切換(由於休眠狀態的存在,使你的執行緒不至於意外退出)。
- RunLoop提供了處理事件源(source0,source1)機制(CFRunLoopSource)。
- RunLoop提供了對Timer的支援(CFRunLoopTimer)。
- RunLoop自身會在多種狀態間切換(run,sleep,exit等),在狀態切換時,RunLoop會通知所註冊的Observer(CFRunLoopObserver),使得系統可以在特定的時機執行對應的操作。相關的如AutoreleasePool 的Pop/Push,手勢識別等。
RunLoop在run時,會進入如下圖所示的do while迴圈:
當RunLoop沒有任務處理時,會進入休眠狀態,此時如果在XCode點選暫停,你會看到主執行緒呼叫棧是停留在 mach_msg_trap() 這個地方。
對應CF原始碼,RunLoop會卡在
ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL);
在mach_msg函式裡面,執行緒會陷入mach_msg_trap。
關於RunLoop的休眠,涉及到一個關鍵點,就是mach port通訊。
以下內容摘自深入理解RunLoop
RunLoop 的核心就是一個 mach_msg() (圖中第7步),RunLoop 呼叫這個函式去接收訊息,如果沒有別人傳送 port 訊息過來,核心會將執行緒置於等待狀態。例如你在模擬器裡跑起一個 iOS 的 App,然後在 App 靜止時點選暫停,你會看到主執行緒呼叫棧是停留在 mach_msg_trap() 這個地方。
為了解釋這個邏輯,下面稍微介紹一下 OSX/iOS 的系統架構。
蘋果官方將整個系統大致劃分為上述4個層次:
應用層包括使用者能接觸到的圖形應用,例如 Spotlight、Aqua、SpringBoard 等。
應用框架層即開發人員接觸到的 Cocoa 等框架。
核心框架層包括各種核心框架、OpenGL 等內容。
Darwin 即作業系統的核心,包括系統核心、驅動、Shell 等內容,這一層是開源的,其所有原始碼都可以在 opensource.apple.com 裡找到。
Darwin的架構如下:
其中,在硬體層上面的三個組成部分:Mach、BSD、IOKit (還包括一些上面沒標註的內容),共同組成了 XNU 核心。
XNU 核心的內環被稱作 Mach,其作為一個微核心,僅提供了諸如處理器排程、IPC (程序間通訊)等非常少量的基礎服務。
BSD 層可以看作圍繞 Mach 層的一個外環,其提供了諸如程序管理、檔案系統和網路等功能。
IOKit 層是為裝置驅動提供了一個面向物件(C++)的一個框架。
在 Mach 中,所有的東西都是通過物件實現的,程序、執行緒和虛擬記憶體都被稱為”物件”。
Mach 的物件間不能直接呼叫,只能通過訊息傳遞的方式實現物件間的通訊。”訊息”是 Mach 中最基礎的概念,訊息在兩個埠 (port) 之間傳遞,這就是 Mach 的 IPC (程序間通訊) 的核心。 其實Mach通訊,可以參考相對熟悉的socket通訊。
好,讓我們回到RunLoop中來。
Thread & RunLoop
雖然RunLoop與Thread的關係十分密切,但是並不是每個Thread都擁有一個RunLoop的。
在iOS中,除了系統會為主執行緒自動建立一個RunLoop,在子執行緒中,我們需要手動獲取執行緒對應的RunLoop:
[NSRunLoop currentRunLoop];
[NSRunLoop mainRunLoop]
// 其對應的CF原始碼為:
CF_EXPORT CFRunLoopRef CFRunLoopGetCurrent(void);
CF_EXPORT CFRunLoopRef CFRunLoopGetMain(void);
方法的名字很具有迷惑性,Get方法可能讓我們以為已經存線上程對應的RunLoop了,其實這是一個懶載入方法,程式碼實現是:
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
static CFRunLoopRef __main = NULL; // no retain needed
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
Get RunLoop函式首先會在在static 的__main runLoop 或當前子執行緒的TSD(程私有資料Thread-specific Data)中去找是否有當前執行緒對應的RunLoop,如果沒有找到,則
這兩個函式最終都會呼叫 _CFRunLoopGet0(pthread_t t)方法,其引數是當前執行緒自身,簡化版實現是:
static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFLock_t loopsLock = CFLockInit;
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
...
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); // 在靜態全域性變數__CFRunLoops尋找執行緒t所對應的RunLoop(key為執行緒自身)
__CFUnlock(&loopsLock);
if (!loop) { // 沒找到
CFRunLoopRef newLoop = __CFRunLoopCreate(t); // 建立一個新的Loop
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); // 為什麼get兩次??
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); // 將新建立的Loop存入__CFRunLoops字典
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是一個執行緒,則同時設定當前執行緒的TSD
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop; // 返回Loop
}
由程式碼可知:
- RunLoop和Thread是一一對應的(key: pthread value:runLoop)
- Thread預設是沒有對應的RunLoop的,僅當主動呼叫Get方法時,才會建立
- 所有Thread執行緒對應的RunLoop被儲存在全域性的__CFRunLoops字典中。同時,主執行緒在static CFRunLoopRef __main,子執行緒在TSD中,也儲存了執行緒對應的RunLoop,用於快速查詢。
這裡有一點要弄清,Thread和RunLoop不是包含關係,而是平等的對應關係。Thread的若干功能,是通過RunLoop實現的。
另一點是,RunLoop自己是不會Run的,需要我們手動呼叫Run方法(Main RunLoop會由系統啟動),我們的RunLoop才會跑圈。靜止(注意,這裡的靜止不是休眠的意思)的RunLoop是不會做任何事情的
RunLoop Mode
上面我們可以知道,RunLoop只有在Run的情況下,才會處理具體事務。
那麼提到Run,這裡就離不開RunLoop Mode。
每次RunLoop開始Run的時候,都必須指定一個Mode,稱為RunLoop Mode。Mode指定了在這次的Run中,RunLoop可以處理的任務。對於不屬於當前Mode的任務,則需要切換RunLoop至對應Mode下,再重新呼叫run方法,才能夠被處理。
這點在CF的程式碼中可以看出,CF中RunLoop的run函式如下:
static int32_t __CFRunLoopRun(CFRunLoopRef rl, // runloop will run
CFRunLoopModeRef rlm, // the runloop 's mode to run
Boolean stopAfterHandle, // if Yes, exit runloop after handle on source
CFRunLoopModeRef previousMode) // previous runloop mode
RunLoop,RunLoop mode和RunLoop items的關係如下圖所示:
一個RunLoop可以有多個mode,每一個mode下都有4個集合結構,分別對應著當前mode可以處理的source0,source1, 和timer事件,以及當前mode下所註冊的RunLoop state observers。
在Mode 的右側類似棒棒糖的圖形上,分別標出了當RunLoop休眠時(mach_msg_trap狀態),外部執行緒/程序可以將RunLoop喚醒的Mach Port。有兩大類,_portSet, 是一個port集合。另一個是_timerPort,專用於NSTimer事件的RunLoop喚醒。(其實在_portSet中是包含了_timePort的)關於RunLoop的休眠和喚醒,我們將在稍後提到。
RunLoop mode的程式碼如下所示:
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue; // dispatch timer 所在的queue
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};
我們可以自定義mode,系統也為我們預定義了一些mode:
這裡我們只需要關心Default 和 Event tracking Mode,以及一個特殊的’mode’ : Common modes。
Default mode 是RunLoop預設的mode。當RunLoop被建立時,就會對應創建出一個default mode。其餘的mode,則是懶載入的。
Event tracking mode 是Cocoa在處理密集傳入的事件時所使用的mode(如scrollview的滑動)。
Common modes 其實不算是一個mode,而是一個mode的集合。在Cocoa程式中,預設會包含default,modal,event tracking mode。而在Core Foundation程式中,預設僅有Default mode。我們也可以將自定義的mode加入到common modes中。如果我們希望將事件能夠在多個mode下得到處理,則直接將事件註冊到Common modes中即可。
這裡,我們稍微在原始碼的角度瞭解下mode是如何加入到common modes中的。
我們回看CFRunLoop定義中和mode相關的結構:
struct __CFRunLoop {
...
CFMutableSetRef _commonModes; // set:被加入到common mode中的mode
CFMutableSetRef _commonModeItems; // set: 被加入到common mode 下的items(source/timer/observer )
CFRunLoopModeRef _currentMode; // 當前的mode
CFMutableSetRef _modes; // RunLoop所有的modes
...
};
一個普通的mode可以將自身‘標記’為common,具體做法是,系統會將該mode的name新增到_commonModes中。
當item被加入到common modes下時,首先,會在runloop的_commonModeItems中新增一個條目,然後,會遍歷所有的_commonModes,將該item加入到已經被標記為common的mode中。
當一個mode作為整體被加入common modes下時,則會進行另外一番操作。系統會呼叫void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName)
方法。在方法內部,系統首先會將mode置位為common(將mode name加入到_commonModes)。然後,會將_commonModeItems中的所有元素,新增到這個mode中。注意,這裡mode中的item並不會被加入到_commonModeItems中。
這意味著,當我們將mode作為整體加入到Common modes中時,該mode可以響應common mode item事件,而其本身自帶的mode items,在別的被標記為common的mode中,卻不會被響應。
當我們將任務交給RunLoop的時候,需要指定任務在哪個mode下面處理,如果不指定,則預設在default mode下處理。
一個任務可以提交到多個mode中,如果向一個mode多次提交同一個任務,則mode中僅會儲存一個任務,因為在程式碼中會有類似的判斷:
if(!CFSetContainsValue(rlm->_sources0, rls)) {
// 將任務加入到mode中
}
如,timer是基於RunLoop實現的,我們在建立timer時,可以指定timer的mode:
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"do timer");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; // 指定timer在common modes(default mode + event tracking mode) 下執行
如果我們沒有指定timer的RunLoop mode,則預設會新增到default mode下執行。
這也就解釋了,為什麼當我們在滑動scrollview的時候,timer事件不會被回撥。因為如果我們將timer新增到預設的主執行緒 的default mode時,當用戶滑動scrollview的時候,main RunLoop 會切換到event tracking mode下來接收處理密集的滑動事件,這時候,新增在default mode下的timer是不會被觸發的。
解決方法就是,我們將timer新增到common modes下,讓其在default mode和Event tracking mode下面都可以被呼叫。
RunLoop Source
蘋果文件將RunLoop能夠處理的事件分為Input sources和timer事件。下面這張圖取自蘋果官網,不要注意那些容易讓人混淆的細節,只看Thread , Input sources 和 Timer sources三個大方塊的關係即可,不要關注裡面的內容。
timer事件我們下面會講到,現在來看Input source。
根據CF的原始碼,Input source在RunLoop中被分類成source0和source1兩大類。source0和source1均有結構體__CFRunLoopSource表示:
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* 優先順序,越小,優先順序越高。可以是負數。immutable */
CFMutableBagRef _runLoops;
union { // 聯合,用於儲存source的資訊,同時可以區分source是0還是1型別
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
typedef struct {
CFIndex version; // 型別:source0
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);
void (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
void (*perform)(void *info); // call out
} CFRunLoopSourceContext;
typedef struct {
CFIndex version; // 型別:source1
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); // call out
#endif
} CFRunLoopSourceContext1;
source0和source1由聯合_context來做程式碼區分。
source0 VS source1
相同
1. 均是__CFRunLoopSource型別,這就像一個協議,我們甚至可以自己拓展__CFRunLoopSource,定義自己的source。
2. 均是需要被Signaled後,才能夠被處理。
3. 處理時,均是呼叫__CFRunLoopSource._context.version(0?1).perform
,其實這就是呼叫一個函式指標。
不同
- source0需要手動signaled,source1系統會自動signaled
- source0需要手動喚醒RunLoop,才能夠被處理:
CFRunLoopWakeUp(CFRunLoopRef rl)
。而source1 會自動喚醒(通過mach port)RunLoop來處理。
Source1 由RunLoop和核心管理,Mach Port驅動。
Source0 則偏向應用層一些,如Cocoa裡面的UIEvent處理,會以source0的形式傳送給main RunLoop。
Timer
我們經常使用的timer有幾種?
- NSTimer & PerformSelector:afterDelay:(由RunLoop處理,內部結構為CFRunLoopTimerRef)
- GCD Timer(由GCD自己實現,不通過RunLoop)
CADisplayLink(通過向RunLoop投遞source1 實現回撥)
NSObject perform系列函式中的dealy型別, 其實也是一種Timer事件,可能不那麼明顯:
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay
-
這種Perform delay的函式底層的實現是和NSTimer一樣的,根據蘋果官方文件所述:
This method sets up a timer to perform the aSelector message on the current thread’s run loop. The timer is configured to run in the default mode (NSDefaultRunLoopMode). When the timer fires, the thread attempts to dequeue the message from the run loop and perform the selector. It succeeds if the run loop is running and in the default mode; otherwise, the timer waits until the run loop is in the default mode.
If you want the message to be dequeued when the run loop is in a mode other than the default mode, use the performSelector:withObject:afterDelay:inModes: method instead.
NSTimer & PerformSelector:afterDelay:
NSTimer在CF原始碼中的結構是這樣的:
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 */ // timer 回撥
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
關於Timer的計時,是通過核心的mach time或GCD time來實現的。
在RunLoop中,NSTimer在啟用時,會將休眠中的RunLoop通過_timerPort喚醒,(如果是通過GCD實現的NSTimer,則會通過另一個CGD queue專用mach port),之後,RunLoop會呼叫
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
來回調到timer的fire函式。
下面是NSTimer觸發時的函式呼叫堆疊:
PerformSelector:afterDelay:有類似的呼叫堆疊,因為底層實現和NSTimer是一樣的。
Observer
Observer在CF中的結構如下:
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities; /*所監聽的事件,通過位異或,可以監聽多種事件 immutable */
CFIndex _order; /* 優先順序 immutable */
CFRunLoopObserverCallBack _callout; /* observer 回撥 immutable */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
Observer的作用是可以讓外部監聽RunLoop的執行狀態,從而根據不同的時機,做一些操作。
系統會在APP啟動時,向main RunLoop裡註冊了兩個 Observer,其回撥都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一個 Observer 監視的事件是 Entry(即將進入Loop),其回撥內會呼叫
_objc_autoreleasePoolPush() 建立自動釋放池。其 order 是-2147483647,優先順序最高,保證建立釋放池發生在其他所有回撥之前。第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠)
時呼叫_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush()
釋放舊的池並建立新池;Exit(即將退出Loop) 時呼叫 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個
Observer 的 order 是 2147483647,優先順序最低,保證其釋放池子發生在其他所有回撥之後。在主執行緒執行的程式碼,通常是寫在諸如事件回撥、Timer回撥內的。這些回撥會被 RunLoop 建立好的 AutoreleasePool
環繞著,所以不會出現記憶體洩漏,開發者也不必顯示建立 Pool 了。
Observer可以監聽的事件在CF中以位異或表示:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 進入RunLoop迴圈(這裡其實還沒進入)
kCFRunLoopBeforeTimers = (1UL << 1), // RunLoop 要處理timer了
kCFRunLoopBeforeSources = (1UL << 2), // RunLoop 要處理source了
kCFRunLoopBeforeWaiting = (1UL << 5), // RunLoop要休眠了
kCFRunLoopAfterWaiting = (1UL << 6), // RunLoop醒了
kCFRunLoopExit = (1UL << 7), // RunLoop退出(和kCFRunLoopEntry對應)
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
這裡,
kCFRunLoopEntry,
kCFRunLoopExit
在每次RunLoop迴圈中僅呼叫一次,用於表示即將進入迴圈和退出迴圈。
kCFRunLoopBeforeTimers,
kCFRunLoopBeforeSources,
kCFRunLoopBeforeWaiting,
kCFRunLoopAfterWaiting
這些通知會在迴圈內部發出,可能會呼叫多次。
關於如何利用觀察RunLoop的狀態,一個檢測介面卡頓的例子:
RunLoop總結:RunLoop的應用場景(四)App卡頓監測
微信iOS卡頓監控系統
RunLoop原始碼剖析
CF的RunLoop原始碼比較長,而且還為了跨平臺引入了一些Windows的邏輯。我這裡刪除了一些非主要的邏輯,並配上註釋,大家可以結合對比真實的程式碼進行理解。
/**
* 執行run loop
*
* @param rl 執行的RunLoop物件
* @param modeName 執行的mode名稱
* @param seconds run loop超時時間
* @param returnAfterSourceHandled true:run loop處理完事件就退出 false:一直執行直到超時或者被手動終止
*
*
* @return 返回4種狀態
*/
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled){
// 根據modeName獲得當前mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
// 儲存上一次mode 並將runloop替換為當前mode
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
int32_t result = kCFRunLoopRunFinished;
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); // 通知observer,進入RunLoop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); // 呼叫 __CFRunLoopRun 真正開始run
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); // 通知observer, runloop退出
rl->_currentMode = previousMode; // 將runloop恢復為之前的mode
return result; // 返回runloop run的返回值
}
在CFRunLoopRunSpecific
的函式裡面,核心的是呼叫__CFRunLoopRun
讓RunLoop真正的run起來:
/**
* 執行run loop
*
* @param rl 執行的RunLoop物件
* @param rlm 執行的mode
* @param seconds run loop超時時間
* @param stopAfterHandle true:run loop處理完事件就退出 false:一直執行直到超時或者被手動終止
* @param previousMode 上一次執行的mode
*
* @return 返回4種狀態
*/
static int32_t __CFRunLoopRun(CFRunLoopRef rl,
CFRunLoopModeRef rlm,
CFTimeInterval seconds,
CFRunLoopModeRef previousMode) {
// 藉助GCD timer,設定runloop的超時時間
dispatch_source_t timeout_timer = ...
// 超時回撥函式 __CFRunLoopTimeout
dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
// 超時取消函式 __CFRunLoopTimeoutCancel
dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
// 進入do while迴圈,開始run
int32_t retVal = 0; // runloop run返回值,預設為0,會在do while中根據情況被修改,當不為0時,runloop退出
do{
mach_port_t livePort = MACH_PORT_NULL; // 用於記錄喚醒休眠的RunLoop的mach port,休眠前是NULL
__CFPortSet waitSet = rlm->_portSet; // 取當前mode所需要監聽的mach port集合,用於喚醒runloop(__CFPortSet 實際上是unsigned int 型別)
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); // 通知 即將處理 Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); // 通知 即將處理 sources
// 處理提交到runloop的blocks
__CFRunLoopDoBlocks(rl, rlm);
// 處理 source0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm); // 處理提交的runloop的block
}
// 如果有source1被signaled,則不休眠,直接跳到handle_msg來處理source1
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); // 通知observer before waiting
// ****** 開始休眠 ******
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); // 呼叫__CFRunLoopServiceMachPort, 監聽waitSet所指定的mach port埠集合, 如果沒有port message,進入 mach_msg_trap, RunLoop休眠,直到收到port message或者超時
// ****** 休眠結束 ******
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); // 通知observer, runloop醒了
handle_msg:; // 處理事件
// 根據喚醒RunLoop的livePort值,來進行對應邏輯處理
if (MACH_PORT_NULL == livePort) { // MACH_PORT_NULL: 可能是休眠超時,啥都不做
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
} else if (livePort == rl->_wakeUpPort) { // rl->_wakeUpPort: 被其他執行緒或程序喚醒,啥都不做
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
// do nothing on Mac OS
} else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) // rlm->_timerPort: 處理nstimer 訊息
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer
__CFArmNextTimerInMode(rlm, rl);
}
}else if (livePort == dispatchPort) // dispatchPort:處理分發到main queue上的事件
{
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}else { // 其餘的,肯定是各種source1 事件
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
if (NULL != reply) { // 如果有需要回復soruce1的訊息,則回覆
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
}
}
// ****** 到這裡就結束了對livePort的處理 ******
__CFRunLoopDoBlocks(rl, rlm); // 處理提交到runloop的blocks
// 檢查runloop是否需要退出
if (sourceHandledThisLoop && stopAfterHandle) { // case1. 指定了僅處理一個source 退出kCFRunLoopRunHandledSource
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) { // case2. RunLoop 超時
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) { // case3. RunLoop 被終止
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) { // case4. RunLoop Mode 被終止
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { // case5. RunLoop Mode裡面沒有任何要被處理的事件了(沒有source0,source1, timer,以及提交到當前runloop mode的block)
retVal = kCFRunLoopRunFinished;
}
}while(0 == retVal);
// runloop迴圈結束,返回退出原因
return retVal;
}
總結一下:
- RunLoop僅會對當前mode下的source,timer和observer進行處理
- RunLoop在各個狀態下會對observer傳送相應通知。通知順序是:
進入RunLoop迴圈前: (1)kCFRunLoopEntry
進入迴圈((2)–(4)反覆迴圈):
(2)kCFRunLoopBeforeTimers -> (3)kCFRunLoopBeforeSources -> (4)kCFRunLoopBeforeWaiting -> (5)kCFRunLoopAfterWaiting
退出迴圈:(6)kCFRunLoopExit - RunLoop通過呼叫
__CFRunLoopServiceMachPort
,通過Mach Port監聽Ports來實現休眠(陷入mach_msg_trap狀態)。可以喚醒RunLoop的事件包括:
(1) Mach Port監聽超時
(2)被其他執行緒\程序喚醒
(3)有Timer事件需要執行
(4)有提交到main queue上的block(當前RunLoop是main RunLoop時才有這種情況)
(5)被source1喚醒 - RunLoop退出的可能原因有:
(1)RunLoop超時
(2)RunLoop Mode超時
(3)RunLoop被終止
(4)RunLoop Mode被終止
(5)RunLoop Mode裡面source0, source1, timer佇列都空了,沒啥要處理了
當我們的APP沒有任何事件處理時,通過XCode的APP暫停鍵,可以看到main RunLoop處於休眠狀態的堆疊:
蘋果用RunLoop實現的功能
事件響應
iOS裝置的事件響應,是有RunLoop參與的。
提起iOS裝置的事件響應,相信大家都會有一個大概的瞭解:
(1)使用者觸發事件->(2)系統將事件轉交到對應APP的事件佇列->(3)APP從訊息佇列頭取出事件->(4)交由Main Window進行訊息分發->(5)找到合適的Responder進行處理,如果沒找到,則會沿著Responder chain返回到APP層,丟棄不響應該事件
這裡涉及到兩個問題,(3)到(5)步是由程序內處理的,而(1)到(2)步則涉及到裝置硬體,iOS作業系統,以及目標APP之間的通訊,通訊的大致步驟是什麼樣的呢?
當我們的APP在接收到任何事件請求之前,main RunLoop都是處於mach_msg_trap休眠狀態中的,那麼,又是誰喚醒它的呢?
好,莫急,讓我們慢慢分析。首先,我們用po命令,打印出APP的main RunLoop(注意,callback所對應的具體函式名稱只能在模擬器中顯示,可能和真機的訪問控制有關)。一堆東西,慢慢看還是能夠看懂的,沒有耐心的同學也可以忽略,直接看下面的分析:
<CFRunLoop 0x6000001f7200 [0x1029cebb0]>{wakeup port = 0x1f03, stopped = false, ignoreWakeUps = false,
current mode = kCFRunLoopDefaultMode,
common modes = <CFBasicHash 0x600000048c40 [0x1029cebb0]>{type = mutable set, count = 2,
entries =>
0 : <CFString 0x103d52820 [0x1029cebb0]>{contents = "UITrackingRunLoopMode"}
2 : <CFString 0x1029a47f0 [0x1029cebb0]>{contents = "kCFRunLoopDefaultMode"}
}
,
common mode items = <CFBasicHash 0x6080000491b0 [0x1029cebb0]>{type = mutable set, count = 14,
entries =>
0 : <CFRunLoopSource 0x608000172300 [0x1029cebb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x10771b6c6)}}
1 : <CFRunLoopSource 0x604000172b40 [0x1029cebb0]>{signalled = No, valid = Yes, order = -2, context = <CFRunLoopSource context>{version = 0, info = 0x6080000495a0, callout = __handleHIDEventFetcherDrain (0x1034e5dd8)}}
2 : <CFRunLoopObserver 0x604000128a20 [0x1029cebb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x102b7d276), context = <CFArray 0x60400044da40 [0x1029cebb0]>{type = mutable-small, count = 1, values = (
0 : <0x7fb024022048>
)}}
3 : <CFRunLoopObserver 0x604000128980 [0x1029cebb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x102bad057), context = <CFRunLoopObserver context 0x7fb023600240>}
4 : <CFRunLoopObserver 0x6040001288e0 [0x1029cebb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x102bacfdc), context = <CFRunLoopObserver context 0x7fb023600240>}
5 : <CFRunLoopObserver 0x604000128ac0 [0x1029cebb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x102b7d276), context = <CFArray 0x60400044da40 [0x1029cebb0]>{type = mutable-small, count = 1, values = (
0 : <0x7fb024022048>
)}}
6 : <CFRunLoopObserver 0x608000127f80 [0x1029cebb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x1087f2648), context = <CFRunLoopObserver context 0x0>}
8 : <CFRunLoopObserver 0x608000127da0 [0x1029cebb0]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x10316b1a9), context = <CFRunLoopObserver context 0x6080000df800>}
11 : <CFRunLoopSource 0x608000172f00 [0x1029cebb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource context>{version = 0, info = 0x6080000b1dc0, callout = FBSSerialQueueRunLoopSourceHandler (0x106e87821)}}
15 : <CFRunLoopSource 0x604000173980 [0x1029cebb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 39943, subsystem = 0x107296f78, context = 0x6080000b1b80}}
17 : <CFRunLoopSource 0x6040001729c0 [0x1029cebb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x60400014e0d0, callout = __handleEventQueue (0x1034e5dcc)}}
18 : <CFRunLoopSource 0x604000172900 [0x1029cebb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 17935, subsystem = 0x103d09ce8, context = 0x0}}
19 : <CFRunLoopSource 0x600000172000 [0x1029cebb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 22535, subsystem = 0x103d24088, context = 0x60000003c180}}
21 : <CFRunLoopSource 0x60c000172900 [0x1029cebb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 1, info = 0x4b03, callout = PurpleEventCallback (0x10771dbef)}}
}
,
// 各種RunLoop Modes
modes = <CFBasicHash