Runloop 是和線程緊密相關(guān)的一個基礎(chǔ)組件,是很多線程有關(guān)功能的幕后功臣蓄愁。盡管在平常使用中幾乎不太會直接用到,理解 Runloop 有利于我們更加深入地理解 iOS 的多線程模型。
本文從如下幾個方面理解RunLoop的相關(guān)知識點限煞。
- RunLoop概念
- RunLoop實現(xiàn)
- RunLoop運行
- RunLoop應(yīng)用
RunLoop概念
RunLoop介紹
RunLoop 是什么?RunLoop 還是比較顧名思義的一個東西员凝,說白了就是一種循環(huán)署驻,只不過它這種循環(huán)比較高級。一般的 while 循環(huán)會導(dǎo)致 CPU 進(jìn)入忙等待狀態(tài)健霹,而 RunLoop 則是一種“閑”等待硕舆,這部分可以類比 Linux 下的 epoll。當(dāng)沒有事件時骤公,RunLoop 會進(jìn)入休眠狀態(tài)抚官,有事件發(fā)生時, RunLoop 會去找對應(yīng)的 Handler 處理事件阶捆。RunLoop 可以讓線程在需要做事的時候忙起來凌节,不需要的話就讓線程休眠。
從代碼上看洒试,RunLoop其實就是一個對象倍奢,它的結(jié)構(gòu)如下,源碼看這里:
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp 內(nèi)核向該端口發(fā)送消息可以喚醒runloop
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread; //RunLoop對應(yīng)的線程
uint32_t _winthread;
CFMutableSetRef _commonModes; //存儲的是字符串垒棋,記錄所有標(biāo)記為common的mode
CFMutableSetRef _commonModeItems;//存儲所有commonMode的item(source卒煞、timer、observer)
CFRunLoopModeRef _currentMode; //當(dāng)前運行的mode
CFMutableSetRef _modes; //存儲的是CFRunLoopModeRef
struct _block_item *_blocks_head;//doblocks的時候用到
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};
可見叼架,一個RunLoop對象畔裕,主要包含了一個線程衣撬,若干個Mode,若干個commonMode扮饶,還有一個當(dāng)前運行的Mode具练。
RunLoop與線程
當(dāng)我們需要一個常駐線程,可以讓線程在需要做事的時候忙起來甜无,不需要的話就讓線程休眠扛点。我們就在線程里面執(zhí)行下面這個代碼丈秩,一直等待消息渡处,線程就不會退出了。
do {
//獲取消息
//處理消息
} while (消息 摸航!= 退出)
上面的這種循環(huán)模型被稱作 Event Loop奥帘,事件循環(huán)模型在眾多系統(tǒng)里都有實現(xiàn)铜邮,RunLoop 實際上就是一個對象,這個對象管理了其需要處理的事件和消息翩概,并提供了一個入口函數(shù)來執(zhí)行上面 Event Loop 的邏輯牲距。線程執(zhí)行了這個函數(shù)后,就會一直處于這個函數(shù)內(nèi)部 "接受消息->等待->處理" 的循環(huán)中钥庇,直到這個循環(huán)結(jié)束(比如傳入 quit 的消息)牍鞠,函數(shù)返回。
下圖描述了Runloop運行流程(基本描述了上面Runloop的核心流程评姨,當(dāng)然可以查看官方The Run Loop Sequence of Events描述):
整個流程并不復(fù)雜(需要注意的就是黃色區(qū)域的消息處理中并不包含source0难述,因為它在循環(huán)開始之初就會處理),整個流程其實就是一種Event Loop的實現(xiàn)吐句,其他平臺均有類似的實現(xiàn)胁后,只是這里叫做RunLoop。
RunLoop與線程的關(guān)系如下圖
圖中展現(xiàn)了 Runloop 在線程中的作用:從 input source 和 timer source 接受事件嗦枢,然后在線程中處理事件攀芯。
Runloop 和線程是綁定在一起的。每個線程(包括主線程)都有一個對應(yīng)的 Runloop 對象文虏。我們并不能自己創(chuàng)建 Runloop 對象侣诺,但是可以獲取到系統(tǒng)提供的 Runloop 對象。
主線程的 Runloop 會在應(yīng)用啟動的時候完成啟動氧秘,其他線程的 Runloop 默認(rèn)并不會啟動年鸳,需要我們手動啟動。
RunLoop Mode
Mode可以視為事件的管家丸相,一個Mode管理著各種事件搔确,它的結(jié)構(gòu)如下:
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name; //mode名稱
Boolean _stopped; //mode是否被終止
char _padding[3];
//幾種事件
CFMutableSetRef _sources0; //sources0
CFMutableSetRef _sources1; //sources1
CFMutableArrayRef _observers; //通知
CFMutableArrayRef _timers; //定時器
CFMutableDictionaryRef _portToV1SourceMap; //字典 key是mach_port_t,value是CFRunLoopSourceRef
__CFPortSet _portSet; //保存所有需要監(jiān)聽的port,比如_wakeUpPort膳算,_timerPort都保存在這個數(shù)組中
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _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 */
};
一個CFRunLoopMode對象有一個name座硕,若干source0、source1畦幢、timer坎吻、observer和若干port缆蝉,可見事件都是由Mode在管理宇葱,而RunLoop管理Mode。
從源碼很容易看出刊头,Runloop總是運行在某種特定的CFRunLoopModeRef下(每次運行__CFRunLoopRun()函數(shù)時必須指定Mode)黍瞧。而通過CFRunloopRef對應(yīng)結(jié)構(gòu)體的定義可以很容易知道每種Runloop都可以包含若干個Mode,每個Mode又包含Source/Timer/Observer原杂。每次調(diào)用Runloop的主函數(shù)__CFRunLoopRun()時必須指定一種Mode印颤,這個Mode稱為 _currentMode,當(dāng)切換Mode時必須退出當(dāng)前Mode穿肄,然后重新進(jìn)入Runloop以保證不同Mode的Source/Timer/Observer互不影響年局。
如圖所示,Runloop Mode 實際上是 Source咸产,Timer 和 Observer 的集合矢否,不同的 Mode 把不同組的 Source,Timer 和 Observer 隔絕開來脑溢。Runloop 在某個時刻只能跑在一個 Mode 下僵朗,處理這一個 Mode 當(dāng)中的 Source,Timer 和 Observer屑彻。
蘋果文檔中提到的 Mode 有五個验庙,分別是:
- NSDefaultRunLoopMode
- NSConnectionReplyMode
- NSModalPanelRunLoopMode
- NSEventTrackingRunLoopMode
- NSRunLoopCommonModes
iOS 中公開暴露出來的只有 NSDefaultRunLoopMode 和 NSRunLoopCommonModes。 NSRunLoopCommonModes 實際上是一個 Mode 的集合社牲,默認(rèn)包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode(注意:并不是說Runloop會運行在kCFRunLoopCommonModes這種模式下粪薛,而是相當(dāng)于分別注冊了 NSDefaultRunLoopMode和 UITrackingRunLoopMode。當(dāng)然你也可以通過調(diào)用CFRunLoopAddCommonMode()方法將自定義Mode放到 kCFRunLoopCommonModes組合)搏恤。
五種Mode的介紹如下圖:
RunLoop Source
Run Loop Source分為Source违寿、Observer、Timer三種挑社,他們統(tǒng)稱為ModeItem陨界。
CFRunLoopSource
根據(jù)官方的描述,CFRunLoopSource是對input sources的抽象痛阻。CFRunLoopSource分source0和source1兩個版本菌瘪,它的結(jié)構(gòu)如下:
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits; //用于標(biāo)記Signaled狀態(tài),source0只有在被標(biāo)記為Signaled狀態(tài),才會被處理
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
source0是App內(nèi)部事件俏扩,由App自己管理的UIEvent糜工、CFSocket都是source0。當(dāng)一個source0事件準(zhǔn)備執(zhí)行的時候录淡,必須要先把它標(biāo)記為signal狀態(tài)捌木,以下是source0的結(jié)構(gòu)體:
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);
void (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
void (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
void (*perform)(void *info);
} CFRunLoopSourceContext;
source0是非基于Port的。只包含了一個回調(diào)(函數(shù)指針)嫉戚,它并不能主動觸發(fā)事件刨裆。使用時,你需要先調(diào)用 CFRunLoopSourceSignal(source)彬檀,將這個 Source 標(biāo)記為待處理帆啃,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件窍帝。
source1由RunLoop和內(nèi)核管理努潘,source1帶有mach_port_t,可以接收內(nèi)核消息并觸發(fā)回調(diào)坤学,以下是source1的結(jié)構(gòu)體
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除了包含回調(diào)指針外包含一個mach port疯坤,Source1可以監(jiān)聽系統(tǒng)端口和通過內(nèi)核和其他線程通信,接收深浮、分發(fā)系統(tǒng)事件压怠,它能夠主動喚醒RunLoop(由操作系統(tǒng)內(nèi)核進(jìn)行管理,例如CFMessagePort消息)略号。官方也指出可以自定義Source刑峡,因此對于CFRunLoopSourceRef來說它更像一種協(xié)議,框架已經(jīng)默認(rèn)定義了兩種實現(xiàn)玄柠,如果有必要開發(fā)人員也可以自定義突梦,詳細(xì)情況可以查看官方文檔。
CFRunLoopObserver
CFRunLoopObserver是觀察者羽利,可以觀察RunLoop的各種狀態(tài)宫患,并拋出回調(diào)。
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 */
};
CFRunLoopObserver可以觀察的狀態(tài)有如下6種:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即將進(jìn)入run loop
kCFRunLoopBeforeTimers = (1UL << 1), //即將處理timer
kCFRunLoopBeforeSources = (1UL << 2),//即將處理source
kCFRunLoopBeforeWaiting = (1UL << 5),//即將進(jìn)入休眠
kCFRunLoopAfterWaiting = (1UL << 6),//被喚醒但是還沒開始處理事件
kCFRunLoopExit = (1UL << 7),//run loop已經(jīng)退出
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
Runloop 通過監(jiān)控 Source 來決定有沒有任務(wù)要做这弧,除此之外娃闲,我們還可以用 Runloop Observer 來監(jiān)控 Runloop 本身的狀態(tài)。 Runloop Observer 可以監(jiān)控上面的 Runloop 事件匾浪,具體流程如下圖皇帮。
CFRunLoopTimer
CFRunLoopTimer是定時器,可以在設(shè)定的時間點拋出回調(diào)蛋辈,它的結(jié)構(gòu)如下:
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits; //標(biāo)記fire狀態(tài)
pthread_mutex_t _lock;
CFRunLoopRef _runLoop; //添加該timer的runloop
CFMutableSetRef _rlModes; //存放所有 包含該timer的 mode的 modeName属拾,意味著一個timer可能會在多個mode中存在
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; //理想時間間隔 /* immutable */
CFTimeInterval _tolerance; //時間偏差 /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
另外根據(jù)官方文檔的描述将谊,CFRunLoopTimer和NSTimer是toll-free bridged的,可以相互轉(zhuǎn)換渐白。
CFRunLoopTimer is “toll-free bridged” with its Cocoa Foundation counterpart, NSTimer. This means that the Core Foundation type is interchangeable in function or method calls with the bridged Foundation object.
所以CFRunLoopTimer具有以下特性:
- CFRunLoopTimer 是定時器尊浓,可以在設(shè)定的時間點拋出回調(diào)
- CFRunLoopTimer和NSTimer是toll-free bridged的,可以相互轉(zhuǎn)換
RunLoop實現(xiàn)
下面從以下3個方面介紹RunLoop的實現(xiàn)纯衍。
- 獲取RunLoop
- 添加Mode
- 添加Run Loop Source
獲取RunLoop
從蘋果開放的API來看栋齿,不允許我們直接創(chuàng)建RunLoop對象,只能通過以下幾個函數(shù)來獲取RunLoop:
- CFRunLoopRef CFRunLoopGetCurrent(void)
- CFRunLoopRef CFRunLoopGetMain(void)
- +(NSRunLoop *)currentRunLoop
- +(NSRunLoop *)mainRunLoop
前兩個是Core Foundation中的API襟诸,后兩個是Foundation中的API。
那么RunLoop是什么時候被創(chuàng)建的呢励堡?
我們從下面幾個函數(shù)內(nèi)部看看应结。
CFRunLoopGetCurrent
//取當(dāng)前所在線程的RunLoop
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
//傳入當(dāng)前線程
return _CFRunLoopGet0(pthread_self());
}
在CFRunLoopGetCurrent函數(shù)內(nèi)部調(diào)用了_CFRunLoopGet0()鹅龄,傳入的參數(shù)是當(dāng)前線程pthread_self()
扮休。這里可以看出玷坠,CFRunLoopGetCurrent函數(shù)必須要在線程內(nèi)部調(diào)用,才能獲取當(dāng)前線程的RunLoop劲藐。也就是說子線程的RunLoop必須要在子線程內(nèi)部獲取八堡。
CFRunLoopGetMain
//取主線程的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;
}
在CFRunLoopGetMain函數(shù)內(nèi)部也調(diào)用了_CFRunLoopGet0(),傳入的參數(shù)是主線程pthread_main_thread_np()
聘芜⌒置欤可以看出,CFRunLoopGetMain()不管在主線程還是子線程中調(diào)用汰现,都可以獲取到主線程的RunLoop挂谍。
CFRunLoopGet0
前面兩個函數(shù)都是使用了CFRunLoopGet0實現(xiàn)傳入線程的函數(shù),下面看下CFRunLoopGet0的結(jié)構(gòu)是咋樣的瞎饲。
static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFSpinLock_t loopsLock = CFSpinLockInit;
// t==0 is a synonym for "main thread" that always works
//根據(jù)線程取RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFSpinLock(&loopsLock);
//如果存儲RunLoop的字典不存在
if (!__CFRunLoops) {
__CFSpinUnlock(&loopsLock);
//創(chuàng)建一個臨時字典dict
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//創(chuàng)建主線程的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
//把主線程的RunLoop保存到dict中口叙,key是線程,value是RunLoop
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
//此處NULL和__CFRunLoops指針都指向NULL嗅战,匹配妄田,所以將dict寫到__CFRunLoops
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
//釋放dict
CFRelease(dict);
}
//釋放mainrunloop
CFRelease(mainLoop);
__CFSpinLock(&loopsLock);
}
//以上說明,第一次進(jìn)來的時候铅辞,不管是getMainRunloop還是get子線程的runloop,主線程的runloop總是會被創(chuàng)建
//從字典__CFRunLoops中獲取傳入線程t的runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
//如果沒有獲取到
if (!loop) {
//根據(jù)線程t創(chuàng)建一個runloop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//把newLoop存入字典__CFRunLoops囤踩,key是線程t
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFSpinUnlock(&loopsLock);
CFRelease(newLoop);
}
//如果傳入線程就是當(dāng)前線程
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
//注冊一個回調(diào)涣仿,當(dāng)線程銷毀時愉镰,銷毀對應(yīng)的RunLoop
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
這段代碼可以得出以下結(jié)論:
- RunLoop和線程的一一對應(yīng)的拔莱,對應(yīng)的方式是以key-value的方式保存在一個全局字典中
- 主線程的RunLoop會在初始化全局字典時創(chuàng)建
- 子線程的RunLoop會在第一次獲取的時候創(chuàng)建讼渊,如果不獲取的話就一直不會被創(chuàng)建
- RunLoop會在線程銷毀時銷毀
添加Mode
在Core Foundation中,針對Mode的操作,蘋果只開放了以下3個API(Cocoa中也有功能一樣的函數(shù)叶组,不再列出):
- CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode)
- CFStringRef CFRunLoopCopyCurrentMode(CFRunLoopRef rl)
- CFArrayRef CFRunLoopCopyAllModes(CFRunLoopRef rl)
CFRunLoopAddCommonMode
Adds a mode to the set of run loop common modes.
向當(dāng)前RunLoop的common modes中添加一個mode船庇。CFRunLoopCopyCurrentMode
Returns the name of the mode in which a given run loop is currently running.
返回當(dāng)前運行的mode的nameCFRunLoopCopyAllModes
Returns an array that contains all the defined modes for a CFRunLoop object.
返回當(dāng)前RunLoop的所有mode
我們沒有辦法直接創(chuàng)建一個CFRunLoopMode對象橄霉,但是我們可以調(diào)用CFRunLoopAddCommonMode傳入一個字符串向RunLoop中添加Mode按厘,傳入的字符串即為Mode的名字束莫,Mode對象應(yīng)該是此時在RunLoop內(nèi)部創(chuàng)建的漓藕。下面來看一下源碼诀蓉。
CFRunLoopAddCommonMode
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
__CFRunLoopLock(rl);
//看rl中是否已經(jīng)有這個mode,如果有就什么都不做
if (!CFSetContainsValue(rl->_commonModes, modeName)) {
CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
//把modeName添加到RunLoop的_commonModes中
CFSetAddValue(rl->_commonModes, modeName);
if (NULL != set) {
CFTypeRef context[2] = {rl, modeName};
/* add all common-modes items to new mode */
//這里調(diào)用CFRunLoopAddSource/CFRunLoopAddObserver/CFRunLoopAddTimer的時候會調(diào)用
//__CFRunLoopFindMode(rl, modeName, true),CFRunLoopMode對象在這個時候被創(chuàng)建
CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
CFRelease(set);
}
} else {
}
__CFRunLoopUnlock(rl);
}
可以看得出:
- modeName不能重復(fù)妓美,modeName是mode的唯一標(biāo)識符
- RunLoop的_commonModes數(shù)組存放所有被標(biāo)記為common的mode的名稱
- 添加commonMode會把commonModeItems數(shù)組中的所有source同步到新添加的mode中
- CFRunLoopMode對象在CFRunLoopAddItemsToCommonMode函數(shù)中調(diào)用CFRunLoopFindMode時被創(chuàng)建
CFRunLoopCopyCurrentMode/CFRunLoopCopyAllModes
CFRunLoopCopyCurrentMode和CFRunLoopCopyAllModes的內(nèi)部邏輯比較簡單,直接取RunLoop的_currentMode和_modes返回贵试,就不貼源碼了。
添加Run Loop Source(ModeItem)
我們可以通過以下接口添加/移除各種事件:
- void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)
- void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)
- void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode)
- void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef * mode)
- void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
- void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
CFRunLoopAddSource
CFRunLoopAddSource的代碼結(jié)構(gòu)如下:
//添加source事件
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
if (!__CFIsValid(rls)) return;
Boolean doVer0Callout = false;
__CFRunLoopLock(rl);
//如果是kCFRunLoopCommonModes
if (modeName == kCFRunLoopCommonModes) {
//如果runloop的_commonModes存在,則copy一個新的復(fù)制給set
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
//如果runl _commonModeItems為空
if (NULL == rl->_commonModeItems) {
//先初始化
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
//把傳入的CFRunLoopSourceRef加入_commonModeItems
CFSetAddValue(rl->_commonModeItems, rls);
//如果剛才set copy到的數(shù)組里有數(shù)據(jù)
if (NULL != set) {
CFTypeRef context[2] = {rl, rls};
/* add new item to all common-modes */
//則把set里的所有mode都執(zhí)行一遍__CFRunLoopAddItemToCommonModes函數(shù)
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
//以上分支的邏輯就是往声,如果你往kCFRunLoopCommonModes里面添加一個source,那么所有_commonModes里的mode都會添加這個source
} else {
//根據(jù)modeName查找mode
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
//如果_sources0不存在慢洋,則初始化_sources0,_sources0和_portToV1SourceMap
if (NULL != rlm && NULL == rlm->_sources0) {
rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);
}
//如果_sources0和_sources1中都不包含傳入的source
if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) {
//如果version是0,則加到_sources0
if (0 == rls->_context.version0.version) {
CFSetAddValue(rlm->_sources0, rls);
//如果version是1,則加到_sources1
} else if (1 == rls->_context.version0.version) {
CFSetAddValue(rlm->_sources1, rls);
__CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
if (CFPORT_NULL != src_port) {
//此處只有在加到source1的時候才會把souce和一個mach_port_t對應(yīng)起來
//可以理解為酿愧,source1可以通過內(nèi)核向其端口發(fā)送消息來主動喚醒runloop
CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
__CFPortSetInsert(src_port, rlm->_portSet);
}
}
__CFRunLoopSourceLock(rls);
//把runloop加入到source的_runLoops中
if (NULL == rls->_runLoops) {
rls->_runLoops = CFBagCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeBagCallBacks); // sources retain run loops!
}
CFBagAddValue(rls->_runLoops, rl);
__CFRunLoopSourceUnlock(rls);
if (0 == rls->_context.version0.version) {
if (NULL != rls->_context.version0.schedule) {
doVer0Callout = true;
}
}
}
if (NULL != rlm) {
__CFRunLoopModeUnlock(rlm);
}
}
__CFRunLoopUnlock(rl);
if (doVer0Callout) {
// although it looses some protection for the source, we have no choice but
// to do this after unlocking the run loop and mode locks, to avoid deadlocks
// where the source wants to take a lock which is already held in another
// thread which is itself waiting for a run loop/mode lock
rls->_context.version0.schedule(rls->_context.version0.info, rl, modeName); /* CALLOUT */
}
}
通過添加source的這段代碼可以得出如下結(jié)論:
- 如果modeName傳入kCFRunLoopCommonModes庞钢,則該source會被保存到RunLoop的_commonModeItems中
- 如果modeName傳入kCFRunLoopCommonModes,則該source會被添加到所有commonMode中
- 如果modeName傳入的不是kCFRunLoopCommonModes阱穗,則會先查找該Mode昌抠,如果沒有,會創(chuàng)建一個
- 同一個source在一個mode中只能被添加一次
CFRunLoopRemoveSource
remove操作和add操作的邏輯基本一致,很容易理解唠梨。
//移除source
void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */
CHECK_FOR_FORK();
Boolean doVer0Callout = false, doRLSRelease = false;
__CFRunLoopLock(rl);
//如果是kCFRunLoopCommonModes,則從_commonModes的所有mode中移除該source
if (modeName == kCFRunLoopCommonModes) {
if (NULL != rl->_commonModeItems && CFSetContainsValue(rl->_commonModeItems, rls)) {
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
CFSetRemoveValue(rl->_commonModeItems, rls);
if (NULL != set) {
CFTypeRef context[2] = {rl, rls};
/* remove new item from all common-modes */
CFSetApplyFunction(set, (__CFRunLoopRemoveItemFromCommonModes), (void *)context);
CFRelease(set);
}
} else {
}
} else {
//根據(jù)modeName查找mode,如果不存在醉箕,返回NULL
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, false);
if (NULL != rlm && ((NULL != rlm->_sources0 && CFSetContainsValue(rlm->_sources0, rls)) || (NULL != rlm->_sources1 && CFSetContainsValue(rlm->_sources1, rls)))) {
CFRetain(rls);
//根據(jù)source版本做對應(yīng)的remove操作
if (1 == rls->_context.version0.version) {
__CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
if (CFPORT_NULL != src_port) {
CFDictionaryRemoveValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port);
__CFPortSetRemove(src_port, rlm->_portSet);
}
}
CFSetRemoveValue(rlm->_sources0, rls);
CFSetRemoveValue(rlm->_sources1, rls);
__CFRunLoopSourceLock(rls);
if (NULL != rls->_runLoops) {
CFBagRemoveValue(rls->_runLoops, rl);
}
__CFRunLoopSourceUnlock(rls);
if (0 == rls->_context.version0.version) {
if (NULL != rls->_context.version0.cancel) {
doVer0Callout = true;
}
}
doRLSRelease = true;
}
if (NULL != rlm) {
__CFRunLoopModeUnlock(rlm);
}
}
__CFRunLoopUnlock(rl);
if (doVer0Callout) {
// although it looses some protection for the source, we have no choice but
// to do this after unlocking the run loop and mode locks, to avoid deadlocks
// where the source wants to take a lock which is already held in another
// thread which is itself waiting for a run loop/mode lock
rls->_context.version0.cancel(rls->_context.version0.info, rl, modeName); /* CALLOUT */
}
if (doRLSRelease) CFRelease(rls);
}
添加Observer和Timer
添加observer和timer的內(nèi)部邏輯和添加source大體類似松邪。
區(qū)別在于observer和timer只能被添加到一個RunLoop的一個或者多個mode中寒亥,比如一個timer被添加到主線程的RunLoop中褂傀,則不能再把該timer添加到子線程的RunLoop同波,而source沒有這個限制,不管是哪個RunLoop,只要mode中沒有悲雳,就可以添加。
這個區(qū)別在文章最開始的結(jié)構(gòu)體中也可以發(fā)現(xiàn)歪玲,CFRunLoopSource結(jié)構(gòu)體中有保存RunLoop對象的數(shù)組,而CFRunLoopObserver和CFRunLoopTimer只有單個RunLoop對象。
RunLoop運行
在Core Foundation中我們可以通過以下2個API來讓RunLoop運行:
- void CFRunLoopRun(void)
在默認(rèn)的mode下運行當(dāng)前線程的RunLoop短条。
- CFRunLoopRunResult CFRunLoopRunInMode(CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled)
在指定mode下運行當(dāng)前線程的RunLoop。
CFRunLoopRun
//默認(rèn)運行runloop的kCFRunLoopDefaultMode
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
//默認(rèn)在kCFRunLoopDefaultMode下運行runloop
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
在CFRunLoopRun函數(shù)中調(diào)用了CFRunLoopRunSpecific函數(shù),runloop參數(shù)傳入當(dāng)前RunLoop對象渠牲,modeName參數(shù)傳入kCFRunLoopDefaultMode。驗證了前面文檔的解釋铣除。
CFRunLoopRunInMode
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
在CFRunLoopRunInMode函數(shù)中也調(diào)用了CFRunLoopRunSpecific函數(shù),runloop參數(shù)傳入當(dāng)前RunLoop對象背苦,modeName參數(shù)繼續(xù)傳遞CFRunLoopRunInMode傳入的modeName钳降。也驗證了前面文檔的解釋铲觉。
這里還可以看出,雖然RunLoop有很多個mode,但是RunLoop在run的時候必須只能指定其中一個mode链烈,運行起來之后,被指定的mode即為currentMode食侮。
這2個函數(shù)都看不出來RunLoop是怎么run起來的誉己。
接下來我們繼續(xù)探索一下CFRunLoopRunSpecific函數(shù)里面都干了什么噪猾,看看RunLoop具體是怎么run的。
CFRunLoopRunSpecific
/*
* 指定mode運行runloop
* @param rl 當(dāng)前運行的runloop
* @param modeName 需要運行的mode的name
* @param seconds runloop的超時時間
* @param returnAfterSourceHandled 是否處理完事件就返回
*/
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
//根據(jù)modeName找到本次運行的mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
//如果沒找到 || mode中沒有注冊任何事件,則就此停止,不進(jìn)入循環(huán)
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
if (currentMode) __CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
//取上一次運行的mode
CFRunLoopModeRef previousMode = rl->_currentMode;
//如果本次mode和上次的mode一致
rl->_currentMode = currentMode;
//初始化一個result為kCFRunLoopRunFinished
int32_t result = kCFRunLoopRunFinished;
// 1.通知observer即將進(jìn)入runloop
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
//10.通知observer已退出runloop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}
通過CFRunLoopRunSpecific的內(nèi)部邏輯,我們可以得出:
- 如果指定了一個不存在的mode來運行RunLoop译打,那么會失敗,mode不會被創(chuàng)建结澄,所以這里傳入的mode必須是存在的
- 如果指定了一個mode猜扮,但是這個mode中不包含任何modeItem齿桃,那么RunLoop也不會運行,所以必須要* 傳入至少包含一個modeItem的mode
- 在進(jìn)入run loop之前通知observer,狀態(tài)為kCFRunLoopEntry
- 在退出run loop之后通知observer千绪,狀態(tài)為kCFRunLoopExit
RunLoop的運行的最核心函數(shù)是__CFRunLoopRun荸型,接下來我們分析__CFRunLoopRun的源碼。
__CFRunLoopRun
這段代碼比較長,請做好心理準(zhǔn)備踪宠,我已經(jīng)加了比較詳細(xì)的注釋。本節(jié)開頭的run loop運行步驟2~9步都在下面的代碼中得到驗證柬脸。
/**
* 運行run loop
*
* @param rl 運行的RunLoop對象
* @param rlm 運行的mode
* @param seconds run loop超時時間
* @param stopAfterHandle true:run loop處理完事件就退出 false:一直運行直到超時或者被手動終止
* @param previousMode 上一次運行的mode
*
* @return 返回4種狀態(tài)
*/
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
//獲取系統(tǒng)啟動后的CPU運行時間爆价,用于控制超時時間
uint64_t startTSR = mach_absolute_time();
//如果RunLoop或者mode是stop狀態(tài)骤宣,則直接return,不進(jìn)入循環(huán)
if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
return kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
return kCFRunLoopRunStopped;
}
//mach端口,在內(nèi)核中锰霜,消息在端口之間傳遞纫事。 初始為0
mach_port_name_t dispatchPort = MACH_PORT_NULL;
//判斷是否為主線程
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
//如果在主線程 && runloop是主線程的runloop && 該mode是commonMode爬立,則給mach端口賦值為主線程收發(fā)消息的端口
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
#if USE_DISPATCH_SOURCE_FOR_TIMERS
mach_port_name_t modeQueuePort = MACH_PORT_NULL;
if (rlm->_queue) {
//mode賦值為dispatch端口_dispatch_runloop_root_queue_perform_4CF
modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
if (!modeQueuePort) {
CRASH("Unable to get port for run loop mode queue (%d)", -1);
}
}
#endif
//GCD管理的定時器,用于實現(xiàn)runloop超時機制
dispatch_source_t timeout_timer = NULL;
struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
//立即超時
if (seconds <= 0.0) { // instant timeout
seconds = 0.0;
timeout_context->termTSR = 0ULL;
}
//seconds為超時時間,超時時執(zhí)行__CFRunLoopTimeout函數(shù)
else if (seconds <= TIMER_INTERVAL_LIMIT) {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_OVERCOMMIT);
timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_retain(timeout_timer);
timeout_context->ds = timeout_timer;
timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
dispatch_resume(timeout_timer);
}
//永不超時
else { // infinite timeout
seconds = 9999999999.0;
timeout_context->termTSR = UINT64_MAX;
}
//標(biāo)志位默認(rèn)為true
Boolean didDispatchPortLastTime = true;
//記錄最后runloop狀態(tài),用于return
int32_t retVal = 0;
do {
//初始化一個存放內(nèi)核消息的緩沖池
uint8_t msg_buffer[3 * 1024];
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
mach_msg_header_t *msg = NULL;
mach_port_t livePort = MACH_PORT_NULL;
#elif DEPLOYMENT_TARGET_WINDOWS
HANDLE livePort = NULL;
Boolean windowsMessageReceived = false;
#endif
//取所有需要監(jiān)聽的port
__CFPortSet waitSet = rlm->_portSet;
//設(shè)置RunLoop為可以被喚醒狀態(tài)
__CFRunLoopUnsetIgnoreWakeUps(rl);
//2.通知observer,即將觸發(fā)timer回調(diào)赂乐,處理timer事件
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
//3.通知observer,即將觸發(fā)Source0回調(diào)
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
//執(zhí)行加入當(dāng)前runloop的block
__CFRunLoopDoBlocks(rl, rlm);
//4.處理source0事件
//有事件處理返回true,沒有事件返回false
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
//執(zhí)行加入當(dāng)前runloop的block
__CFRunLoopDoBlocks(rl, rlm);
}
//如果沒有Sources0事件處理 并且 沒有超時担租,poll為false
//如果有Sources0事件處理 或者 超時,poll都為true
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
//第一次do..whil循環(huán)不會走該分支尝艘,因為didDispatchPortLastTime初始化是true
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
//從緩沖區(qū)讀取消息
msg = (mach_msg_header_t *)msg_buffer;
//5.接收dispatchPort端口的消息,(接收source1事件)
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {
//如果接收到了消息的話娄徊,前往第9步開始處理msg
goto handle_msg;
}
#elif DEPLOYMENT_TARGET_WINDOWS
if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
goto handle_msg;
}
#endif
}
didDispatchPortLastTime = false;
//6.通知觀察者RunLoop即將進(jìn)入休眠
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
//設(shè)置RunLoop為休眠狀態(tài)
__CFRunLoopSetSleeping(rl);
// do not do any user callouts after this point (after notifying of sleeping)
// Must push the local-to-this-activation ports in on every loop
// iteration, as this mode could be run re-entrantly and we don't
// want these ports to get serviced.
__CFPortSetInsert(dispatchPort, waitSet);
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
//這里有個內(nèi)循環(huán)橄仆,用于接收等待端口的消息
//進(jìn)入此循環(huán)后,線程進(jìn)入休眠您宪,直到收到新消息才跳出該循環(huán)铣猩,繼續(xù)執(zhí)行run loop
do {
if (kCFUseCollectableAllocator) {
objc_clear_stack(0);
memset(msg_buffer, 0, sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;
//7.接收waitSet端口的消息
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
//收到消息之后,livePort的值為msg->msgh_local_port龄寞,
if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
// Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
if (rlm->_timerFired) {
// Leave livePort as the queue port, and service timers below
rlm->_timerFired = false;
break;
} else {
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
}
} else {
// Go ahead and leave the inner loop.
break;
}
} while (1);
#else
if (kCFUseCollectableAllocator) {
objc_clear_stack(0);
memset(msg_buffer, 0, sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
#endif
#elif DEPLOYMENT_TARGET_WINDOWS
// Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages.
__CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived);
#endif
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
// Must remove the local-to-this-activation ports in on every loop
// iteration, as this mode could be run re-entrantly and we don't
// want these ports to get serviced. Also, we don't want them left
// in there if this function returns.
__CFPortSetRemove(dispatchPort, waitSet);
__CFRunLoopSetIgnoreWakeUps(rl);
// user callouts now OK again
//取消runloop的休眠狀態(tài)
__CFRunLoopUnsetSleeping(rl);
//8.通知觀察者runloop被喚醒
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
//9.處理收到的消息
handle_msg:;
__CFRunLoopSetIgnoreWakeUps(rl);
#if DEPLOYMENT_TARGET_WINDOWS
if (windowsMessageReceived) {
// These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
if (rlm->_msgPump) {
rlm->_msgPump();
} else {
MSG msg;
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
sourceHandledThisLoop = true;
// To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced
// Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later.
// NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling.
__CFRunLoopSetSleeping(rl);
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
__CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL);
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
__CFRunLoopUnsetSleeping(rl);
// If we have a new live port then it will be handled below as normal
}
#endif
if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
//通過CFRunloopWake喚醒
} else if (livePort == rl->_wakeUpPort) {
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
//什么都不干,跳回2重新循環(huán)
// do nothing on Mac OS
#if DEPLOYMENT_TARGET_WINDOWS
// Always reset the wake up port, or risk spinning forever
ResetEvent(rl->_wakeUpPort);
#endif
}
#if USE_DISPATCH_SOURCE_FOR_TIMERS
//如果是定時器事件
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
//9.1 處理timer事件
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer, because we apparently fired early
__CFArmNextTimerInMode(rlm, rl);
}
}
#endif
#if USE_MK_TIMER_TOO
//如果是定時器事件
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
// On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
// In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
//9.1處理timer事件
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer
__CFArmNextTimerInMode(rlm, rl);
}
}
#endif
//如果是dispatch到main queue的block
else if (livePort == dispatchPort) {
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
#if DEPLOYMENT_TARGET_WINDOWS
void *msg = 0;
#endif
//9.2執(zhí)行block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
sourceHandledThisLoop = true;
didDispatchPortLastTime = true;
} else {
CFRUNLOOP_WAKEUP_FOR_SOURCE();
// Despite the name, this works for windows handles as well
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
// 有source1事件待處理
if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
mach_msg_header_t *reply = NULL;
//9.2 處理source1事件
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
if (NULL != reply) {
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
}
#elif DEPLOYMENT_TARGET_WINDOWS
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
#endif
}
}
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#endif
__CFRunLoopDoBlocks(rl, rlm);
if (sourceHandledThisLoop && stopAfterHandle) {
//進(jìn)入run loop時傳入的參數(shù)蝌矛,處理完事件就返回
retVal = kCFRunLoopRunHandledSource;
}else if (timeout_context->termTSR < mach_absolute_time()) {
//run loop超時
retVal = kCFRunLoopRunTimedOut;
}else if (__CFRunLoopIsStopped(rl)) {
//run loop被手動終止
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
}else if (rlm->_stopped) {
//mode被終止
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
}else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
//mode中沒有要處理的事件
retVal = kCFRunLoopRunFinished;
}
//除了上面這幾種情況走趋,都繼續(xù)循環(huán)
} while (0 == retVal);
if (timeout_timer) {
dispatch_source_cancel(timeout_timer);
dispatch_release(timeout_timer);
} else {
free(timeout_context);
}
return retVal;
}
__CFRunLoopServiceMachPort
第7步調(diào)用了__CFRunLoopServiceMachPort函數(shù)鉴吹,這個函數(shù)在run loop中起到了至關(guān)重要的作用夺荒,下面給出了詳細(xì)注釋。
/**
* 接收指定內(nèi)核端口的消息
*
* @param port 接收消息的端口
* @param buffer 消息緩沖區(qū)
* @param buffer_size 消息緩沖區(qū)大小
* @param livePort 暫且理解為活動的端口,接收消息成功時候值為msg->msgh_local_port丽旅,超時時為MACH_PORT_NULL
* @param timeout 超時時間,單位是ms祷蝌,如果超時茅撞,則RunLoop進(jìn)入休眠狀態(tài)
*
* @return 接收消息成功時返回true 其他情況返回false
*/
static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout) {
Boolean originalBuffer = true;
kern_return_t ret = KERN_SUCCESS;
for (;;) { /* In that sleep of death what nightmares may come ... */
mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
msg->msgh_bits = 0; //消息頭的標(biāo)志位
msg->msgh_local_port = port; //源(發(fā)出的消息)或者目標(biāo)(接收的消息)
msg->msgh_remote_port = MACH_PORT_NULL; //目標(biāo)(發(fā)出的消息)或者源(接收的消息)
msg->msgh_size = buffer_size; //消息緩沖區(qū)大小,單位是字節(jié)
msg->msgh_id = 0; //唯一id
if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
//通過mach_msg發(fā)送或者接收的消息都是指針巨朦,
//如果直接發(fā)送或者接收消息體米丘,會頻繁進(jìn)行內(nèi)存復(fù)制,損耗性能
//所以XNU使用了單一內(nèi)核的方式來解決該問題靶累,所有內(nèi)核組件都共享同一個地址空間急黎,因此傳遞消息時候只需要傳遞消息的指針
ret = mach_msg(msg,
MACH_RCV_MSG|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);
CFRUNLOOP_WAKEUP(ret);
//接收/發(fā)送消息成功绳军,給livePort賦值為msgh_local_port
if (MACH_MSG_SUCCESS == ret) {
*livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
return true;
}
//MACH_RCV_TIMEOUT
//超出timeout時間沒有收到消息顷蟆,返回MACH_RCV_TIMED_OUT
//此時釋放緩沖區(qū)豁生,把livePort賦值為MACH_PORT_NULL
if (MACH_RCV_TIMED_OUT == ret) {
if (!originalBuffer) free(msg);
*buffer = NULL;
*livePort = MACH_PORT_NULL;
return false;
}
//MACH_RCV_LARGE
//如果接收緩沖區(qū)太小豌骏,則將過大的消息放在隊列中,并且出錯返回MACH_RCV_TOO_LARGE衰抑,
//這種情況下蜻底,只返回消息頭搏嗡,調(diào)用者可以分配更多的內(nèi)存
if (MACH_RCV_TOO_LARGE != ret) break;
//此處給buffer分配更大內(nèi)存
buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
if (originalBuffer) *buffer = NULL;
originalBuffer = false;
*buffer = realloc(*buffer, buffer_size);
}
HALT;
return false;
}
小結(jié)
RunLoop實際很簡單延赌,它是一個對象,它和線程是一一對應(yīng)的,每個線程都有一個對應(yīng)的RunLoop對象刺下,主線程的RunLoop會在程序啟動時自動創(chuàng)建蟋恬,子線程需要手動獲取來創(chuàng)建豹绪。
RunLoop運行的核心是一個do..while..循環(huán)啦膜,遍歷所有需要處理的事件灯萍,如果有事件處理就讓線程工作运沦,沒有事件處理則讓線程休眠,同時等待事件到來。
RunLoop應(yīng)用
在開發(fā)過程中幾乎所有的操作都是通過Call out進(jìn)行回調(diào)的(無論是Observer的狀態(tài)通知還是Timer画侣、Source的處理)沙咏,而系統(tǒng)在回調(diào)時通常使用如下幾個函數(shù)進(jìn)行回調(diào)(換句話說你的代碼其實最終都是通過下面幾個函數(shù)來負(fù)責(zé)調(diào)用的,即使你自己監(jiān)聽Observer也會先調(diào)用下面的函數(shù)然后間接通知你陨晶,所以在調(diào)用堆棧中經(jīng)忱齑看到這些函數(shù)):
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();
實際的代碼塊如下:
{
/// 1. 通知Observers哲嘲,即將進(jìn)入RunLoop
/// 此處有Observer會創(chuàng)建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即將觸發(fā) Timer 回調(diào)私恬。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即將觸發(fā) Source (非基于port的,Source0) 回調(diào)。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 觸發(fā) Source0 (非基于port的) 回調(diào)蓉媳。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers绑咱,即將進(jìn)入休眠
/// 此處有Observer釋放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,線程被喚醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer喚醒的,回調(diào)Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch喚醒的,執(zhí)行所有調(diào)用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件喚醒了,處理這個事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers敢辩,即將退出RunLoop
/// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
例如在控制器的touchBegin中打入斷點查看堆棧(由于UIEvent是Source0,所以可以看到一個Source0的Call out函數(shù)CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION調(diào)用):
NSTimer 與 GCD Timer、CADisplayLink
NSTimer
前面一直提到Timer Source作為事件源,事實上它的上層對應(yīng)就是NSTimer(其實就是CFRunloopTimerRef)這個開發(fā)者經(jīng)常用到的定時器(底層基于使用mk_timer實現(xiàn))
NSTimer 其實就是 CFRunLoopTimerRef歧譬,他們之間是 toll-free bridged 的。一個 NSTimer 注冊到 RunLoop 后搏存,RunLoop 會為其重復(fù)的時間點注冊好事件瑰步。例如 10:00, 10:10, 10:20 這幾個時間點。RunLoop為了節(jié)省資源璧眠,并不會在非常準(zhǔn)確的時間點回調(diào)這個Timer缩焦。Timer 有個屬性叫做 Tolerance (寬容度)桑李,標(biāo)示了當(dāng)時間點到后陕悬,容許有多少最大誤差竟闪。由于 NSTimer 的這種機制壶唤,因此 NSTimer 的執(zhí)行必須依賴于 RunLoop,如果沒有 RunLoop地回,NSTimer 是不會執(zhí)行的排宰。
如果某個時間點被錯過了平夜,例如執(zhí)行了一個很長的任務(wù)腰鬼,則那個時間點的回調(diào)也會跳過去嵌赠,不會延后執(zhí)行靴拱。就比如等公交,如果 10:10 時我忙著玩手機錯過了那個點的公交猾普,那我只能等 10:20 這一趟了。
GCD Timer
GCD 則不同本谜,GCD 的線程管理是通過系統(tǒng)來直接管理的初家。GCD Timer 是通過 dispatch port 給 RunLoop 發(fā)送消息,來使 RunLoop 執(zhí)行相應(yīng)的 block乌助,如果所在線程沒有 RunLoop溜在,那么 GCD 會臨時創(chuàng)建一個線程去執(zhí)行 block,執(zhí)行完之后再銷毀掉他托,因此 GCD 的 Timer 是不依賴 RunLoop 的掖肋。
至于這兩個 Timer 的準(zhǔn)確性問題,如果不在 RunLoop 的線程里面執(zhí)行赏参,那么只能使用 GCD Timer志笼,由于 GCD Timer 是基于 MKTimer(mach kernel timer),已經(jīng)很底層了把篓,因此是很準(zhǔn)確的纫溃。
如果在 RunLoop 的線程里面執(zhí)行,由于 GCD Timer 和 NSTimer 都是通過 port 發(fā)送消息的機制來觸發(fā) RunLoop 的韧掩,因此準(zhǔn)確性差別應(yīng)該不是很大紊浩。如果線程 RunLoop 阻塞了,不管是 GCD Timer 還是 NSTimer 都會存在延遲問題疗锐。
CADisplayLink
CADisplayLink是一個執(zhí)行頻率(fps)和屏幕刷新相同(可以修改preferredFramesPerSecond改變刷新頻率)的定時器坊谁,它也需要加入到RunLoop才能執(zhí)行。與NSTimer類似滑臊,CADisplayLink同樣是基于CFRunloopTimerRef實現(xiàn)口芍,底層使用mk_timer(可以比較加入到RunLoop前后RunLoop中timer的變化)。和NSTimer相比它精度更高(盡管NSTimer也可以修改精度)简珠,不過和NStimer類似的是如果遇到大任務(wù)它仍然存在丟幀現(xiàn)象阶界。通常情況下CADisaplayLink用于構(gòu)建幀動畫,看起來相對更加流暢聋庵,而NSTimer則有更廣泛的用處膘融。
AutoreleasePool
AutoreleasePool是另一個與RunLoop相關(guān)討論較多的話題。其實從RunLoop源代碼分析祭玉,AutoreleasePool與RunLoop并沒有直接的關(guān)系氧映,之所以將兩個話題放到一起討論最主要的原因是因為在iOS應(yīng)用啟動后會注冊兩個Observer管理和維護(hù)AutoreleasePool。不妨在應(yīng)用程序剛剛啟動時打印currentRunLoop可以看到系統(tǒng)默認(rèn)注冊了很多個Observer脱货,其中有兩個Observer的callout都是** _ wrapRunLoopWithAutoreleasePoolHandler**岛都,這兩個是和自動釋放池相關(guān)的兩個監(jiān)聽律姨。
<CFRunLoopObserver 0x6080001246a0 [0x101f81df0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
<CFRunLoopObserver 0x608000124420 [0x101f81df0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
第一個Observer會監(jiān)聽RunLoop的進(jìn)入,它會回調(diào)objc_autoreleasePoolPush()向當(dāng)前的AutoreleasePoolPage增加一個哨兵對象標(biāo)志創(chuàng)建自動釋放池臼疫。這個Observer的order是-2147483647優(yōu)先級最高择份,確保發(fā)生在所有回調(diào)操作之前。
第二個Observer會監(jiān)聽RunLoop的進(jìn)入休眠和即將退出RunLoop兩種狀態(tài)烫堤,在即將進(jìn)入休眠時會調(diào)用objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根據(jù)情況從最新加入的對象一直往前清理直到遇到哨兵對象荣赶。而在即將退出RunLoop時會調(diào)用objc_autoreleasePoolPop() 釋放自動自動釋放池內(nèi)對象。這個Observer的order是2147483647鸽斟,優(yōu)先級最低拔创,確保發(fā)生在所有回調(diào)操作之后。
主線程的其他操作通常均在這個AutoreleasePool之內(nèi)(main函數(shù)中)富蓄,以盡可能減少內(nèi)存維護(hù)操作(當(dāng)然你如果需要顯式釋放【例如循環(huán)】時可以自己創(chuàng)建AutoreleasePool否則一般不需要自己創(chuàng)建)剩燥。
其實在應(yīng)用程序啟動后系統(tǒng)還注冊了其他Observer(例如即將進(jìn)入休眠時執(zhí)行注冊回調(diào)_UIGestureRecognizerUpdateObserver用于手勢處理、回調(diào)為_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer用于界面實時繪制更新)和多個Source1(例如context為CFMachPort的Source1用于接收硬件事件響應(yīng)進(jìn)而分發(fā)到應(yīng)用程序一直到UIEvent)立倍。
在主線程執(zhí)行的代碼灭红,通常是寫在諸如事件回調(diào)、Timer回調(diào)內(nèi)的口注。這些回調(diào)會被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著比伏,所以不會出現(xiàn)內(nèi)存泄漏,開發(fā)者也不必顯示創(chuàng)建 Pool 了疆导。
自動釋放池的創(chuàng)建和釋放赁项,銷毀的時機如下所示
- kCFRunLoopEntry; // 進(jìn)入runloop之前,創(chuàng)建一個自動釋放池
- kCFRunLoopBeforeWaiting; // 休眠之前澈段,銷毀自動釋放池悠菜,創(chuàng)建一個新的自動釋放池
- kCFRunLoopExit; // 退出runloop之前,銷毀自動釋放池
事件響應(yīng)
蘋果注冊了一個 Source1 (基于 mach port 的) 用來接收系統(tǒng)事件败富,其回調(diào)函數(shù)為 __IOHIDEventSystemClientQueueCallback()悔醋。
當(dāng)一個硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由 IOKit.framework 生成一個 IOHIDEvent 事件并由 SpringBoard 接收兽叮。這個過程的詳細(xì)情況可以參考這里芬骄。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸鹦聪,加速账阻,接近傳感器等幾種 Event,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進(jìn)程泽本。隨后蘋果注冊的那個 Source1 就會觸發(fā)回調(diào)淘太,并調(diào)用 _UIApplicationHandleEventQueue() 進(jìn)行應(yīng)用內(nèi)部的分發(fā)。
_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理并包裝成 UIEvent 進(jìn)行處理或分發(fā),其中包括識別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow 等蒲牧。通常事件比如 UIButton 點擊撇贺、touchesBegin/Move/End/Cancel 事件都是在這個回調(diào)中完成的。
手勢識別
當(dāng)上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時冰抢,其首先會調(diào)用 Cancel 將當(dāng)前的 touchesBegin/Move/End 系列回調(diào)打斷松嘶。隨后系統(tǒng)將對應(yīng)的 UIGestureRecognizer 標(biāo)記為待處理。
蘋果注冊了一個 Observer 監(jiān)測 BeforeWaiting (Loop即將進(jìn)入休眠) 事件挎扰,這個Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver()喘蟆,其內(nèi)部會獲取所有剛被標(biāo)記為待處理的 GestureRecognizer,并執(zhí)行GestureRecognizer的回調(diào)鼓鲁。
當(dāng)有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時,這個回調(diào)都會進(jìn)行相應(yīng)處理港谊。
UI更新
如果打印App啟動之后的主線程RunLoop可以發(fā)現(xiàn)另外一個callout為_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer骇吭,這個監(jiān)聽專門負(fù)責(zé)UI變化后的更新,比如修改了frame歧寺、調(diào)整了UI層級(UIView/CALayer)或者手動設(shè)置了setNeedsDisplay/setNeedsLayout之后就會將這些操作提交到全局容器燥狰。而這個Observer監(jiān)聽了主線程RunLoop的即將進(jìn)入休眠和退出狀態(tài),一旦進(jìn)入這兩種狀態(tài)則會遍歷所有的UI更新并提交進(jìn)行實際繪制更新斜筐。
這個函數(shù)內(nèi)部的調(diào)用棧大概是這樣的:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];
通常情況下這種方式是完美的龙致,因為除了系統(tǒng)的更新,還可以利用setNeedsDisplay等方法手動觸發(fā)下一次RunLoop運行的更新顷链。但是如果當(dāng)前正在執(zhí)行大量的邏輯運算可能UI的更新就會比較卡目代,因此facebook推出了AsyncDisplayKit來解決這個問題。AsyncDisplayKit其實是將UI排版和繪制運算盡可能放到后臺嗤练,將UI的最終更新操作放到主線程(這一步也必須在主線程完成)榛了,同時提供一套類UIView或CALayer的相關(guān)屬性,盡可能保證開發(fā)者的開發(fā)習(xí)慣煞抬。這個過程中AsyncDisplayKit在主線程RunLoop中增加了一個Observer監(jiān)聽即將進(jìn)入休眠和退出RunLoop兩種狀態(tài),收到回調(diào)時遍歷隊列中的待處理任務(wù)一一執(zhí)行霜大。
NSURLConnection
一旦啟動NSURLConnection以后就會不斷調(diào)用delegate方法接收數(shù)據(jù),這樣一個連續(xù)的的動作正是基于RunLoop來運行革答。
一旦NSURLConnection設(shè)置了delegate會立即創(chuàng)建一個線程com.apple.NSURLConnectionLoader战坤,同時內(nèi)部啟動RunLoop并在NSDefaultMode模式下添加4個Source0。其中CFHTTPCookieStorage用于處理cookie ;CFMultiplexerSource負(fù)責(zé)各種delegate回調(diào)并在回調(diào)中喚醒delegate內(nèi)部的RunLoop(通常是主線程)來執(zhí)行實際操作残拐。
早期版本的AFNetworking庫也是基于NSURLConnection實現(xiàn)途茫,為了能夠在后臺接收delegate回調(diào)AFNetworking內(nèi)部創(chuàng)建了一個空的線程并啟動了RunLoop,當(dāng)需要使用這個后臺線程執(zhí)行任務(wù)時AFNetworking通過**performSelector: onThread: **將這個任務(wù)放到后臺線程的RunLoop中溪食。
當(dāng)調(diào)用 performSelector:onThread: 時慈省,實際上其會創(chuàng)建一個 Timer 加到對應(yīng)的線程去,同樣的,如果對應(yīng)線程沒有 RunLoop 該方法也會失效边败。
GCD和RunLoop的關(guān)系
在RunLoop的源代碼中可以看到用到了GCD的相關(guān)內(nèi)容袱衷,但是RunLoop本身和GCD并沒有直接的關(guān)系。當(dāng)調(diào)用了dispatch_async(dispatch_get_main_queue(), <#^(void)block#>)時libDispatch會向主線程RunLoop發(fā)送消息喚醒RunLoop笑窜,RunLoop從消息中獲取block致燥,并且在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE回調(diào)里執(zhí)行這個block。不過這個操作僅限于主線程排截,其他線程dispatch操作是全部由libDispatch驅(qū)動的嫌蚤。
更多RunLoop的實踐
滾動Scrollview導(dǎo)致定時器失效
在界面上有一個UIScrollview控件,如果此時還有一個定時器在執(zhí)行一個事件断傲,你會發(fā)現(xiàn)當(dāng)你滾動Scrollview的時候脱吱,定時器會失效。
- (void)viewDidLoad {
[super viewDidLoad];
[self timer1];
[self timer2];
}
//下面兩種添加定時器的方法效果相同认罩,都是在主線程中添加定時器
- (void)timer1 {
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopDefaultModes];
}
- (void)timer2 {
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
}
因為當(dāng)你滾動Scrollview的時候箱蝠,RunLoop會切換到UITrackingRunLoopMode 模式,而定時器運行在defaultMode下面垦垂,系統(tǒng)一次只能處理一種模式的RunLoop宦搬,所以導(dǎo)致defaultMode下的定時器失效。
解決方法:
- 把timer注冊到NSRunLoopCommonModes劫拗,它包含了defaultMode和trackingMode兩種模式间校。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
- 使用GCD創(chuàng)建定時器,GCD創(chuàng)建的定時器不會受RunLoop的影響
// 獲得隊列
dispatch_queue_t queue = dispatch_get_main_queue();
// 創(chuàng)建一個定時器(dispatch_source_t本質(zhì)還是個OC對象)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 設(shè)置定時器的各種屬性(幾時開始任務(wù)页慷,每隔多長時間執(zhí)行一次)
// GCD的時間參數(shù)憔足,一般是納秒(1秒 == 10的9次方納秒)
// 比當(dāng)前時間晚1秒開始執(zhí)行
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
//每隔一秒執(zhí)行一次
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interval, 0);
// 設(shè)置回調(diào)
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"------------%@", [NSThread currentThread]);
});
// 啟動定時器
dispatch_resume(self.timer);
圖片下載
由于圖片渲染到屏幕需要消耗較多資源,為了提高用戶體驗酒繁,當(dāng)用戶滾動Tableview的時候四瘫,只在后臺下載圖片,但是不顯示圖片欲逃,當(dāng)用戶停下來的時候才顯示圖片找蜜。
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
上面的代碼可以達(dá)到如下效果:
用戶點擊屏幕,在主線程中稳析,三秒之后顯示圖片洗做,但是當(dāng)用戶點擊屏幕之后,如果此時用戶又開始滾動textview彰居,那么就算過了三秒诚纸,圖片也不會顯示出來,當(dāng)用戶停止了滾動陈惰,才會顯示圖片畦徘。
這是因為限定了方法setImage只能在NSDefaultRunLoopMode 模式下使用。而滾動textview的時候,程序運行在tracking模式下面井辆,所以方法setImage不會執(zhí)行关筒。
常駐線程
需要創(chuàng)建一個在后臺一直存在的程序,來做一些需要頻繁處理的任務(wù)杯缺。比如檢測網(wǎng)絡(luò)狀態(tài)等蒸播。
默認(rèn)情況一個線程創(chuàng)建出來,運行完要做的事情萍肆,線程就會消亡袍榆。而程序啟動的時候,就創(chuàng)建的主線程已經(jīng)加入到RunLoop塘揣,所以主線程不會消亡包雀。
這個時候我們就需要把自己創(chuàng)建的線程加到RunLoop中來,就可以實現(xiàn)線程常駐后臺亲铡。
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
}
- (void)run
{
NSLog(@"----------run----%@", [NSThread currentThread]);
@autoreleasepool{
/*如果不加這句才写,會發(fā)現(xiàn)runloop創(chuàng)建出來就掛了,因為runloop如果沒有CFRunLoopSourceRef事件源輸入或者定時器奴愉,就會立馬消亡。
下面的方法給runloop添加一個NSport铁孵,就是添加一個事件源锭硼,也可以添加一個定時器,或者observer蜕劝,讓runloop不會掛掉*/
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
// 方法1 ,2檀头,3實現(xiàn)的效果相同,讓runloop無限期運行下去
[[NSRunLoop currentRunLoop] run];
}
// 方法2
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
// 方法3
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
NSLog(@"---------");
}
- (void)test
{
NSLog(@"----------test----%@", [NSThread currentThread]);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
}
- (void)run
{
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
如果沒有實現(xiàn)添加NSPort或者NSTimer岖沛,會發(fā)現(xiàn)執(zhí)行完run方法暑始,線程就會消亡,后續(xù)再執(zhí)行touchbegan方法無效婴削。
我們必須保證線程不消亡廊镜,才可以在后臺接受時間處理
RunLoop 啟動前內(nèi)部必須要有至少一個 Timer/Observer/Source,所以在 [runLoop run] 之前先創(chuàng)建了一個新的 NSMachPort 添加進(jìn)去了唉俗。通常情況下嗤朴,調(diào)用者需要持有這個 NSMachPort (mach_port) 并在外部線程通過這個 port 發(fā)送消息到 RunLoop 內(nèi);但此處添加 port 只是為了讓 RunLoop 不至于退出虫溜,并沒有用于實際的發(fā)送消息雹姊。
可以發(fā)現(xiàn)執(zhí)行完了run方法,這個時候再點擊屏幕衡楞,可以不斷執(zhí)行test方法吱雏,因為線程self.thread一直常駐后臺,等待事件加入其中,然后執(zhí)行歧杏。
觀察事件狀態(tài)镰惦,優(yōu)化性能
假設(shè)我們想實現(xiàn)cell的高度緩存計算,因為“計算cell的預(yù)緩存高度”的任務(wù)需要在最無感知的時刻進(jìn)行得滤,所以應(yīng)該同時滿足:
- RunLoop 處于“空閑”狀態(tài) Mode
- 當(dāng)這一次 RunLoop 迭代處理完成了所有事件陨献,馬上要休眠時
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFStringRef runLoopMode = kCFRunLoopDefaultMode;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
// TODO here
});
CFRunLoopAddObserver(runLoop, observer, runLoopMode);
在其中的 TODO 位置,就可以開始任務(wù)的收集和分發(fā)了懂更,當(dāng)然眨业,不能忘記適時的移除這個 observer