[翻譯] Run, RunLoop, Run!

注:這篇文章翻譯自 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));

BlockCFRunLoopPerformBlock()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è)單程惹资,示例著處理上面所討論的事件類型贺纲。

  1. 調(diào)用 “block”。(CFRunLoopPerformBlock() API)
  2. 檢查 Version 0 Sources褪测,如果必要的話調(diào)用它們的 “perform” 方法猴誊。
  3. Poll and internal dispatch queues and mach_ports, and (這句不知道怎么翻譯,感覺(jué)有筆誤)
  4. 如果沒(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。
  5. 被喚醒汛兜,并且嘗試搞清楚原因:
    1. 手動(dòng)喚醒巴粪。僅僅是繼續(xù)運(yùn)行這個(gè) loop,可能有一個(gè) block 或者 Version 0 Source 等待服務(wù)。
    2. 一個(gè)或多個(gè) timer 發(fā)動(dòng)了肛根。調(diào)用它們的方法辫塌。
    3. GCD 需要工作。通過(guò)一個(gè)特殊的 “4CF” dispatch_queue API 來(lái)調(diào)用它派哲。
    4. 內(nèi)核給一個(gè) Version 1 Source 發(fā)了一個(gè)信號(hào)臼氨。找到并且給他服務(wù)。
  6. 再次調(diào)用 “block”芭届。
  7. 檢查退出條件储矩。(Finished, Stopped, TimedOut, HandledSource)
  8. 全部重新開始。

吁褂乍。是不是很簡(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)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末谬运,一起剝皮案震驚了整個(gè)濱河市隙赁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌梆暖,老刑警劉巖伞访,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異轰驳,居然都是意外死亡厚掷,警方通過(guò)查閱死者的電腦和手機(jī)霍掺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門渣触,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事构罗∠亟常” “怎么了鲜屏?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵吱抚,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我豁延,道長(zhǎng)昙篙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任诱咏,我火速辦了婚禮苔可,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘袋狞。我一直安慰自己焚辅,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布苟鸯。 她就那樣靜靜地躺著同蜻,像睡著了一般。 火紅的嫁衣襯著肌膚如雪早处。 梳的紋絲不亂的頭發(fā)上湾蔓,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天,我揣著相機(jī)與錄音砌梆,去河邊找鬼默责。 笑死,一個(gè)胖子當(dāng)著我的面吹牛咸包,可吹牛的內(nèi)容都是我干的桃序。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼烂瘫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼媒熊!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起坟比,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤芦鳍,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后温算,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體怜校,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡间影,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年注竿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡巩割,死狀恐怖裙顽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情宣谈,我是刑警寧澤愈犹,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站闻丑,受9級(jí)特大地震影響漩怎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜嗦嗡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一勋锤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧侥祭,春花似錦叁执、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至胎署,卻和暖如春吆录,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背琼牧。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工径筏, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人障陶。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓滋恬,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親抱究。 傳聞我的和親對(duì)象是個(gè)殘疾皇子恢氯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容