注:這篇文章翻譯自 http://bou.io/RunRunLoopRun.html 翎承,僅供學(xué)習(xí)參考蛉鹿,謝絕轉(zhuǎn)載取劫,已獲得作者 Nicolas Bouilleaud 授權(quán)巷帝。
iOS 中有一個(gè)話題很少被開發(fā)者們提起忌卤,盡管它是所有 app 中最重要的組成元素之一,它就是 Runloop楞泼。Runloop 就像是 app 的心臟驰徊,你的代碼因?yàn)橛兴胚\(yùn)行起來(lái)。
Runloop 的基本原則實(shí)際上很簡(jiǎn)單堕阔,在 iOS 和 OS X 上棍厂,CFRunloop 實(shí)現(xiàn)了被所有高層消息和調(diào)度 API 所使用的核心機(jī)制。
Runloop 到底是什么超陆?
簡(jiǎn)單來(lái)說(shuō)牺弹,runloop 是一個(gè)消息發(fā)送機(jī)制,用于異步的或者線程內(nèi)的通信时呀。它可以被看做一個(gè)信箱张漂,等待消息并把消息發(fā)送出去。
Runloop 主要干兩件事:
- 等待事件的發(fā)生(例如:消息到達(dá))谨娜,
- 發(fā)送消息給它的接收者航攒。
在其他平臺(tái)上,這個(gè)機(jī)制被稱作“Message Pump”趴梢。
Runloop 把可交互的 app 和命令行程序區(qū)分開來(lái)漠畜。命令行程序帶著參數(shù)啟動(dòng)币他,執(zhí)行它們的命令,然后退出盆驹≡驳ぃ可交互的 app 等待用戶的輸入,反應(yīng)躯喇,然后繼續(xù)等待辫封。事實(shí)上,這個(gè)基本的機(jī)制在長(zhǎng)時(shí)間運(yùn)行的進(jìn)程中也能找到廉丽。在服務(wù)器中的倦微,一個(gè) while(1){select();}
就可以看做 runloop。
Runloop 的工作是等待事情發(fā)生正压。這些事情可以是外部的事件欣福,由用戶或系統(tǒng)產(chǎn)生(例如網(wǎng)路請(qǐng)求)或者內(nèi)部的 app 消息,例如線程內(nèi)的通知焦履,代碼的異步執(zhí)行拓劝,定時(shí)器...... 一旦一個(gè)事件(或者說(shuō)消息)被接收,runloop 就會(huì)找到相應(yīng)的監(jiān)聽者并把消息發(fā)送給它嘉裤。
一個(gè)基本的 runloop 實(shí)際上很容易實(shí)現(xiàn)郑临。下面是簡(jiǎn)單的偽代碼:
func postMessage(runloop, message)
{
runloop.queue.pushBack(message)
runloop.signal()
}
func run(runloop)
{
do {
runloop.wait()
message = runloop.queue.popFront()
dispatch(message)
} while(true)
}
秉承著這個(gè)簡(jiǎn)單的機(jī)制,每個(gè)線程會(huì) run()
它自己的 runloop屑宠,和其他線程的 runloop 通過(guò) postMessage() 方法交換消息厢洞。我的同事 Cyril Mottier 向我指出 Android 的實(shí)現(xiàn) 不像那樣復(fù)雜。
iOS 和 OS X 中又如何呢典奉?
在蘋果的系統(tǒng)中躺翻,這是 CFRunloop 的工作,是一個(gè)更高級(jí)的變體 卫玖。你寫的所有代碼都是在某個(gè)時(shí)刻被 CFRunloop 調(diào)用的公你,除了提前的初始化,或者你自己創(chuàng)建線程假瞬。(據(jù)我所知省店,GCD 隊(duì)列自動(dòng)創(chuàng)建的線程不需要 CFRunloop,但是也必然需要一個(gè)消息系統(tǒng)來(lái)方便重用笨触。)
CFRunloop 最重要的特點(diǎn)是 CFRunLoopModes懦傍。CFRunloop 和一系統(tǒng)的“Run Loop Sources”一起工作。Sources 被注冊(cè)到 runloop 的一個(gè)或多個(gè) mode 中芦劣,runloop 被要求在一個(gè)指定的 mode 下運(yùn)行粗俱。當(dāng)一個(gè)事件到達(dá) sources 時(shí),當(dāng)且僅當(dāng) source 的 mode 和 runloop 的當(dāng)前 mode 相同時(shí)虚吟,事件才會(huì)被 runloop 處理寸认。
另外签财,CFRunloop 可以從應(yīng)用代碼中重新進(jìn)入,要么從你自己的代碼中偏塞,要么從 framework 中唱蒸。因?yàn)橐粋€(gè)線程只有一個(gè) CFRunloop,當(dāng)一個(gè)元素想要在一個(gè)特定的 mode 下運(yùn)行時(shí)灸叼,它需要調(diào)用 CFRunLoopRunInMode()
神汹。所有沒(méi)有注冊(cè)進(jìn)這個(gè) mode 的 sources 會(huì)被停止服務(wù)。通常來(lái)說(shuō)古今,那個(gè)元素最終會(huì)把控制權(quán)交給之前的 mode屁魏。
CFRunloop 定義了一個(gè)虛擬的 mode 稱作 “common modes”(KCFRunloopCommonModes),它實(shí)際上是包含了 app 用到的一系列“常用”的 mode捉腥。比如氓拼,main runloop 在 kCFRunLoopCommonModes
下運(yùn)行。
另一方面抵碟,UIKit 定義了一個(gè)特殊的 runloop mode桃漾,叫做 UITrackingRunLoopMode
。當(dāng)對(duì) controls 的追蹤發(fā)生時(shí)拟逮,例如觸摸事件撬统,就會(huì)用到這個(gè) mode。這很重要唱歧,因?yàn)檫@就是 tableview 流暢滾動(dòng)的原因宪摧。當(dāng)主線程的 runloop 在 UITrackingRunLoopMode
下運(yùn)行時(shí)粒竖,大多數(shù)的后臺(tái)事件颅崩,例如網(wǎng)絡(luò)請(qǐng)求,就不會(huì)被發(fā)送了蕊苗。就像這樣沿后,沒(méi)有其他的工作在進(jìn)行,滑動(dòng)也沒(méi)有延遲朽砰。(至少這時(shí)候應(yīng)該是你的問(wèn)題了尖滚。)
簡(jiǎn)單理解 CFRunloop
如果你曾經(jīng)調(diào)試過(guò) iOS 程序的堆棧信息,你應(yīng)該已經(jīng)發(fā)現(xiàn)瞧柔,在堆棧信息的里面漆弄,所有的消息都以 CFRUNLOOP_IS_CALLING_OUT
開頭。當(dāng) CFRunloop 調(diào)出程序代碼時(shí)造锅,它喜歡讓它們顯示出來(lái)撼唾。在 CFRunloop.c 里定義了六個(gè)這樣的函數(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__();
相信你猜到了,這些函數(shù)沒(méi)有其他用途除了幫助調(diào)試堆棧信息哥蔚。CFRunloop 保證了所有的程序代碼都會(huì)調(diào)用其中某個(gè)函數(shù)倒谷。
讓我們一個(gè)一個(gè)來(lái)看蛛蒙。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(
CFRunLoopObserverCallBack func,
CFRunLoopObserverRef observer,
CFRunLoopActivity activity,
void *info);
Observer 有點(diǎn)特殊。CFRunLoopObserber
API 讓你能夠觀察 CFRunloop 的行為并且收到它活動(dòng)的通知渤愁,例如當(dāng)它在處理事件牵祟,當(dāng)它進(jìn)入休眠等等。這對(duì)調(diào)試來(lái)說(shuō)起了很大的作用抖格,你通常在你的 app 中不需要它诺苹,但是當(dāng)你想實(shí)驗(yàn) CFRunloop 的特性時(shí)它就很有幫助了。[2014-10-2 更細(xì):事實(shí)上他挎,它在其他的地方也有作用筝尾,例如 CoreAnimation 通過(guò) Observser 的調(diào)出運(yùn)行。它能夠保證所有的 UI 代碼已經(jīng)開始運(yùn)行办桨,它會(huì)一次性的執(zhí)行所有動(dòng)畫筹淫。]
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(
void (^block)(void));
Block 是 CFRunLoopPerformBlock()
API 的反面,當(dāng)你想在下個(gè)循環(huán)里執(zhí)行代碼時(shí)很有用呢撞。
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(
void *msg);
Main Dispatch Queue 當(dāng)然就是 CFRunloop 和 GCD 溝通的標(biāo)志损姜。很顯然,至少在主線程中殊霞,GCD 和 CFRunloop 是手把手工作的摧阅。盡管 GCD 可以創(chuàng)建一個(gè)沒(méi)有 CFRunloop 的線程,當(dāng)有一個(gè)時(shí)绷蹲,它會(huì)把自己塞進(jìn)去棒卷。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(
CFRunLoopTimerCallBack func,
CFRunLoopTimerRef timer,
void *info);
Timer 相對(duì)來(lái)說(shuō)就很明了了。在 iOS 和 OS X 中祝钢,高層的 timer比规,例如 NSTimer 或者 performSelector:afterDelay:
是用 CFRunloop 的 timer 實(shí)現(xiàn)的。從 iOS 7 和 Mavericks 開始拦英,timer 開始的時(shí)間有一個(gè)容忍度蜒什,這個(gè)特性也是 CFRunloop 提供的。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(
void (*perform)(void *),
void *info);
CFRunloopSources
“Version 0” 和 “Version 1” 事實(shí)上是很不同的東西疤估,盡管它們有相同的 API灾常。Version 0 Sources 只是簡(jiǎn)單的應(yīng)用內(nèi)的消息傳遞機(jī)制,并且必須由程序代碼手動(dòng)的處理铃拇。在給一個(gè) Version 0 Source(通過(guò) CFRunLoopSourceSignal())發(fā)送信號(hào)后钞瀑,CFRunloop 必須被喚醒(通過(guò) CFRunLoopWakeUp())來(lái)處理這個(gè) source。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(
void *(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info),
mach_msg_header_t *msg, CFIndex size, mach_msg_header_t **reply,
void (*perform)(void *),
void *info);
Version 1 Sources慷荔,另一方面來(lái)說(shuō)雕什,使用 math_port 處理內(nèi)核事件。這實(shí)際上是 CFRunloop 的核心:大多數(shù)時(shí)候,當(dāng)你的 app 什么也沒(méi)干监徘,它其實(shí)是在一個(gè) mach_msg(…,MACH_RCV_MSG,…)
調(diào)用里阻塞著晋修。如果你用 Activity Monitor 來(lái)觀察一個(gè)任何一個(gè) app,你很大程度上會(huì)看到下面的東西:
2718 CFRunLoopRunSpecific (in CoreFoundation) + 296 [0x7fff98bb7cb8]
2718 __CFRunLoopRun (in CoreFoundation) + 1371 [0x7fff98bb845b]
2718 __CFRunLoopServiceMachPort (in CoreFoundation) + 212 [0x7fff98bb8f94]
2718 mach_msg (in libsystem_kernel.dylib) + 55 [0x7fff99cf469f]
2718 mach_msg_trap (in libsystem_kernel.dylib) + 10 [0x7fff99cf552e]
代碼在 CFRunloop 的這里凰盔,就在這代碼的上面幾行墓卦,蘋果工程師注釋了來(lái)自 Hamlet soliloquy 和這相關(guān)的引言:
/* In that sleep of death what nightmares may come ... */
CFRunloop.c 的一瞥
在你 app 運(yùn)行的任何時(shí)候,CFRunloop 的核心就是 __CFRunLoopRun()
方法户敬,被公共 API 方法 CFRunLoopRun()
和 CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled)
調(diào)用落剪。
__CFRunLoopRun()
會(huì)因?yàn)樗姆N原因退出:
-
kCFRunLoopRunTimedOut
:在超時(shí)后,如果規(guī)定了間隔的話尿庐, -
kCFRunLoopRunFinished
:當(dāng)它變?yōu)榭盏暮笾也溃纾械?Source 都被移除了抄瑟。 -
kCFRunLoopRunHandledSource
:當(dāng)一個(gè)事件被處理后凡泣,并且攜帶著returnAfterSourceHandled
標(biāo)志。 -
kCFRunLoopRunStopped
:被手動(dòng)用CFRunLoopStop()
停止皮假。
直到其中的一個(gè)原因發(fā)生鞋拟,它會(huì)持續(xù)等待和發(fā)送事件。這里有一個(gè)單程惹资,示例著處理上面所討論的事件類型贺纲。
- 調(diào)用 “block”。(CFRunLoopPerformBlock() API)
- 檢查 Version 0 Sources褪测,如果必要的話調(diào)用它們的 “perform” 方法猴誊。
- Poll and internal dispatch queues and
mach_port
s, and (這句不知道怎么翻譯,感覺(jué)有筆誤) - 如果沒(méi)有事件在等待就休眠侮措。如果有事件就把它喚醒懈叹。其實(shí)在代碼里面更復(fù)雜,因?yàn)樵?Win32 的兼容代碼里加了很多
#ifdef
#elif
萝毛,并且在代碼中部有一個(gè) goto项阴。這里的主要想法是滑黔,mach_msg()
可以被配置來(lái)等待多個(gè)隊(duì)列和 port笆包。CFRunloop 通過(guò)這個(gè)來(lái)等同時(shí)待 timer,GCD 調(diào)度略荡,手動(dòng)喚醒庵佣,或者 Version 1 Sources。 - 被喚醒汛兜,并且嘗試搞清楚原因:
- 手動(dòng)喚醒巴粪。僅僅是繼續(xù)運(yùn)行這個(gè) loop,可能有一個(gè) block 或者 Version 0 Source 等待服務(wù)。
- 一個(gè)或多個(gè) timer 發(fā)動(dòng)了肛根。調(diào)用它們的方法辫塌。
- GCD 需要工作。通過(guò)一個(gè)特殊的 “4CF” dispatch_queue API 來(lái)調(diào)用它派哲。
- 內(nèi)核給一個(gè) Version 1 Source 發(fā)了一個(gè)信號(hào)臼氨。找到并且給他服務(wù)。
- 再次調(diào)用 “block”芭届。
- 檢查退出條件储矩。(Finished, Stopped, TimedOut, HandledSource)
- 全部重新開始。
吁褂乍。是不是很簡(jiǎn)單持隧。正如你所知道的,CoreFoundation 是用 C 實(shí)現(xiàn)的逃片,看起來(lái)不怎么現(xiàn)代屡拨。在讀這個(gè)的時(shí)候,我的第一反應(yīng)是 “哇褥实,這需要重構(gòu)”洁仗。另一方面,這代碼是經(jīng)過(guò)測(cè)驗(yàn)的性锭,所以我并不期望它會(huì)很快用 Swift 重寫赠潦。
有一個(gè)代碼模式我最近幾年一直在用,特別是在測(cè)試的時(shí)候草冈。它就是“運(yùn)行 runloop 直到條件變?yōu)?true”她奥,這是任何異步單元測(cè)試的基礎(chǔ)。從以前到現(xiàn)在怎棱,我可能已經(jīng)寫了很多這樣的代碼哩俭,直接用 NSRunloop 或者 CFRunloop 來(lái)獲取,使用超時(shí)時(shí)間等等∪担現(xiàn)在我應(yīng)該可以寫一個(gè)正規(guī)的版本了凡资,下篇文章見(jiàn)。