Runloop
Runloop是iOS系統(tǒng)中的事件循環(huán),它保證了我們的程序不會在main函數(shù)執(zhí)行完后就被退出,(線程保活),可以粗糙地理解成一個
while(true)的循環(huán),但它的實現(xiàn)并沒有那么簡單。實際上它是一個NSRunLoop的對象,在對象內(nèi)部維護了一個事件循環(huán),當沒有事件要處理時,Runloop將線程控制器交給系統(tǒng),即從用戶態(tài)->內(nèi)核態(tài),當被喚醒時又從內(nèi)核態(tài)->用戶態(tài),實現(xiàn)了在休眠時不占用CPU資源。
基礎(chǔ)概念
正如前面的引言提到的,一般程序運行完畢后就會自動退出,比如當我們在Xcode中新建一個macOS的CommandLine項目,當main函數(shù)return后程序即運行完畢并退出。
然而,我們的APP顯然不能這樣,所以我們要讓APP可以隨時響應(yīng)而不退出。這樣的機制通常使用事件循環(huán)(Event Loop)來實現(xiàn),在iOS中即為Runloop。
與Runtime不同的是,Runloop是一個可實際獲取的對象,對應(yīng)Foundation框架的NSRunloop類與Core Foundation框架的CFRunloop類,NSRunloop是基于CFRunloop的上層封裝。
Runloop核心
前面我們提到,Runloop可以簡單地概括成一個while(true)的循環(huán),但實際上這樣的實現(xiàn)會使CPU進行大量無謂的空轉(zhuǎn)。所以,Runloop機制的核心就是保證線程在有events需要處理時能喚醒,在沒有events時能進行休眠。
而實現(xiàn)真正的休眠,是靠沒有events時從用戶態(tài)->內(nèi)核態(tài)實現(xiàn)的,當有事件時,系統(tǒng)內(nèi)核通過mach_msg()或者mach port方法將事件發(fā)送給對應(yīng)的Runloop,Runloop收到事件后從休眠狀態(tài)切換到喚醒狀態(tài),并從內(nèi)核態(tài)->用戶態(tài)。
如何喚醒Runloop
Source
Source是Runloop中一個重要的概念,它代表了在上文中提到的events。
在Runloop中,Source分為兩類
- Source0:該類Source是App的內(nèi)部事件,不具有獨立喚醒Runloop的能力。一個Source0需要被處理時,他需要被
CFRunLoopSourceSignal()函數(shù)標記為待處理,并調(diào)用CFRunLoopWakeUp函數(shù)來喚醒Runloop,CFRunLoopWakeUp函數(shù)內(nèi)部通過一個_wakeUpPort成員變量來喚醒Runloop,推測該變量是一個mach port,Runloop只有通過mach port與mach_msg()才可以喚醒。喚醒后通過調(diào)用__CFRunLoopDoSources0函數(shù)來處理Source0事件,并在之后將該事件標記為已處理。 - Source1:該類Source是由硬件事件生成的Source,如觸摸、搖晃、旋轉(zhuǎn)等。此類Source可喚醒Runloop。
Timer
使用NSTimer API注冊執(zhí)行的任務(wù),就屬于這一類
Observer
某個Observer可以監(jiān)聽runloop的狀態(tài)變化,并作出反應(yīng)
Runloop與線程的關(guān)系
- Runloop與線程是一一對應(yīng)的,且子線程的Runloop無法獲取到其他子線程的Runloop,一一對應(yīng)的關(guān)系以key-value存儲在一個全局字典里。在
CFRunloop中,只有CFRunloopGetMain和CFRunloopGetCurrent兩個函數(shù)可以獲取到Runloop - 主線程會自動創(chuàng)建Runloop以響應(yīng)事件,但子線程并不會自動創(chuàng)建Runloop。由于NSTimer對象需要加入到Runloop中的mode,所以在子線程中調(diào)用
performSel afterdelay系列方法并不會被調(diào)用,因為這些方法都會注冊一個NSTimer到Runloop中,而子線程默認情況下是沒有Runloop的。 - Runloop會在線程銷毀時銷毀
CFRunLoopMode
mode是管理著Runloop與source/timer/observer之間的橋梁,在一開始會注冊五個mode
- kCFRunLoopDefaultMode: App的默認 Mode,通常主線程是在這個 Mode 下運行的。
- UITrackingRunLoopMode: 界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響。默認NSTimer是被加入到default mode中的,所以當滑動時Runloop切換到tracking mode,這時default mode中的Timer回調(diào)不會被調(diào)用,所以NSTimer的精度沒有CADisplayLinker高。
- UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用。
- GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到。
- kCFRunLoopCommonModes: 這是一個占位的 Mode,沒有實際作用。
- 如果需要將事件加入到多個mode中,則將它注冊到commonMode中,該mode實際上是多個mode的集合。
- 出于將source/timer/observer分隔開的目的,RunLoop一次只能運行在一個mode下,當運行時在RunLoop的currentMode屬性中會標記當前運行的mode。而當要切換mode時,RunLoop必須先退出,并選中一個mode重新進入,達到切換mode的目的。在切換mode時,被加入到commonModes中的事件會被拷貝一次到運行的mode中。
源碼驗證
__CFRunLoop
RunLoop在Core Foundation中對應(yīng)的類是CFRunLoopRef,其對應(yīng)的結(jié)構(gòu)是__CFRunLoop
可以看到結(jié)構(gòu)體中包含了上文提到的mode,對應(yīng)CFRunLoopModeRef,其結(jié)構(gòu)如下圖
在這里我們看到了上文提到的source,observer,timer,這是能喚醒RunLoop的三種類型,當然能獨立喚醒RunLoop的只有sources1. Mode與source,observer,timer的關(guān)系如下圖
一個 RunLoop 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer。每次調(diào)用 RunLoop 的主函數(shù)時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode。如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進入。這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響。
_CFRunloopGet0()
在Core Foundation中,提供了兩個接口來獲取Runloop,分別是CFRunloopGetMain和CFRunloopGetCurrent,先來看看他們的源碼實現(xiàn)。
可以看到,兩個函數(shù)實際上都是調(diào)用了_CFRunLoopGet0()方法,方法的參數(shù)是線程pthread_t。在CFRunLoopGetCurrent()中,如果當前線程的Runloop已存在,那么會在_CFGetTSD()函數(shù)中找到并返回。
接下來繼續(xù)看看_CFRunLoopGet0()函數(shù),顯然這是獲取RunLoop的關(guān)鍵函數(shù)。先看一下第一部分。
這里注意到__CFRunLoops變量,它是一個CFMutableDictionaryRef類型的字典,key為線程,value為CFRunLoopRef
在第一次進入時,_CFRunLoopGet0()函數(shù)先創(chuàng)建了CFMutableDictionaryRef類型的字典變量,顯然這個就是全局RunLoop表了。這里也印證了前文提到的線程與RunLoop一一對應(yīng)的結(jié)論。
接著我們可以看到,這里調(diào)用__CFRunLoopCreate()函數(shù)創(chuàng)建了主線程的RunLoop,所以RunLoop是直到被獲取時才會被創(chuàng)建,如果不獲取便不會被創(chuàng)建。RunLoop創(chuàng)建后以key為線程,value為CFRunLoopRef存儲在全局RunLoop表中。
接著看看第二部分,如果不是第一次進入則是走到這個流程的代碼。
首先定義了一個loop變量,從全局RunLoops表__CFRunLoops中查找線程對應(yīng)的RunLoop,如果找到了則返回該CFRunLoopRef。
如果在__CFRunLoops中沒有查找到該線程對應(yīng)的RunLoop,則調(diào)用__CFRunLoopCreate()函數(shù)創(chuàng)建RunLoop,并添加到__CFRunLoops中。
__CFRunLoopSource
在mode中,Source對應(yīng)__CFRunLoopSource結(jié)構(gòu)體。在上文我們知道Source是分為Source0和Source1的,而他們其實都是__CFRunLoopSource,在結(jié)構(gòu)體中,以CFRunLoopSourceContext來區(qū)分不同的Source,其中的version0和version1分別對應(yīng)source0和source1。
上圖即為CFRunLoopSourceContext和CFRunLoopSourceContext1的結(jié)構(gòu)定義,可以看到CFRunLoopSourceContext1與CFRunLoopSourceContext0一個明顯的區(qū)別就是CFRunLoopSourceContext1具有一個mach_port_t類型的變量。從這里就可以知道為什么Source0不可以獨立喚醒RunLoop而Source1可以,在前文中我們提到只有mach port和mach_msg()可以獨立喚醒RunLoop。
__CFRunLoopCreate(_CFThreadRef t)
該函數(shù)被用來創(chuàng)建RunLoop,在_CFRunloopGet0()函數(shù)中若獲取不到線程對應(yīng)的RunLoop則調(diào)用該函數(shù)來創(chuàng)建一個新RunLoop??梢钥吹饺?yún)⑹且粋€_CFThreadRef類型的變量,代表著線程,因為線程與RunLoop是一一對應(yīng)的。
在該函數(shù)中進行了對__CFRunLoop結(jié)構(gòu)的分配內(nèi)存與初始化,可以看到實際上是調(diào)用了_CFRuntimeCreateInstance()函數(shù)創(chuàng)建了CFRunLoopRef類型的實例,從該函數(shù)的名字以及代碼可以看到,其實RunLoop的創(chuàng)建是利用Runtime的動態(tài)創(chuàng)建類的特性來創(chuàng)建的。
CFRunLoopWakeUp
該函數(shù)在CFRunLoop中用來喚醒RunLoop,可以看到在TARGET_OS_MAC下,函數(shù)的關(guān)鍵調(diào)用是__CFSendTrivialMachMessage()函數(shù),該函數(shù)使用了CFRunLoop中的_wakeUpPort屬性。
可以看到在__CFSendTrivialMachMessage函數(shù)內(nèi)部,的確是使用了mach_msg的方式來給mach port發(fā)送信息,以達到喚醒RunLoop的目的。
Runloop應(yīng)用之事件響應(yīng)
從用戶觸摸屏幕,到我們的app響應(yīng)這個觸摸,中間其實需要經(jīng)過多步的處理,并且涉及到的是硬件->軟件的通信。之前關(guān)于Runloop的
Source中提到Source1是一類由硬件生成的事件,那么以觸摸事件為例子,看看Runloop是怎么處理事件響應(yīng)的。
事件響應(yīng)鏈
首先我們來梳理一下事件響應(yīng)鏈。
- 用戶觸發(fā)事件
- 系統(tǒng)將事件轉(zhuǎn)交到對應(yīng) APP 的事件隊列
- APP 從消息隊列頭取出事件
- 交由 Main Window 進行消息分發(fā)
- 找到合適的 Responder 進行處理,如果沒找到,則會沿著 Responder chain 返回到 APP 層,丟棄不響應(yīng)該事件。
從上面的五步,我們可以看到其實1和2是獨立于app外的,需要涉及到硬件,從第3步開始事件才被發(fā)送到app內(nèi)進行處理。
用戶觸發(fā)事件
這一步始于用戶點觸屏幕,此時系統(tǒng)的IOKit.framework會生成一個IOHIDEvent事件,該事件會被Spring board接收。
事件轉(zhuǎn)發(fā)到app
當IOHIDEvent被接收后,將會通過mach port轉(zhuǎn)發(fā)給對應(yīng)的app進程。而這時,app中一個名為com.apple.uikit.eventfetch-thread的線程中已經(jīng)注冊了一個Source1,其回調(diào)是__IOHIDEventSystemClientQueueCallback()函數(shù)。
消息隊列
IOHIDEvent被mach port轉(zhuǎn)發(fā)到對應(yīng)app進程后,就喚醒了com.apple.uikit.eventfetch-thread線程的Runloop,并調(diào)用了Source1對應(yīng)的__IOHIDEventSystemClientQueueCallback()回調(diào)。該回調(diào)從消息隊列中取出event,并將main thread__handleEventQueue對應(yīng)的Source0設(shè)置為待處理狀態(tài),同時喚醒main thread的Runloop。
消息分發(fā)
此時就開始調(diào)用__eventQueueSourceCallback函數(shù)進行消息分發(fā),_UIApplicationHandleEventQueue會把IOHIDEvent封裝成UIEvent并分發(fā)出去,開始hitTest等函數(shù)的調(diào)用,并且TouchBegan/end/move/cancel等函數(shù)都是在該回調(diào)中調(diào)用的。
In short
用戶觸發(fā)事件, IOKit.framework 生成一個 IOHIDEvent 事件并由 SpringBoard接收,SpringBoard 會利用 mach port,產(chǎn)生 source1,來喚醒目標 APP 的 com.apple.uikit.eventfetch-thread 的 RunLoop。Eventfetch thread 會將 main runloop 中 __handleEventQueue 所對應(yīng)的 source0 設(shè)置為 signalled = Yes 狀態(tài),同時喚醒 main RunLoop。mainRunLoop 則調(diào)用 __eventQueueSourceCallback進行事件隊列處理。
Runloop與autorelease pool
Runloop會注冊幾個監(jiān)聽自己狀態(tài)變化的回調(diào),當Runloop進入一次事件循環(huán)時,會調(diào)用AutoreleasePoolPage::push()方法創(chuàng)建一個新的自動釋放池并傳入哨兵對象,而在即將退出或者休眠時則會將自動釋放池中的對象pop出。
寫在最后
運行的主要函數(shù)CFRunLoopRun()有點長...留待下次分析
如有錯漏,歡迎指出
Tino Wu
more at tinowu.top