以問答的形式介紹以下內(nèi)容:從線程的角度理解 RunLoop,RunLoop Mode 的設(shè)計(jì)機(jī)制及使用技巧,以 RunLoop 為基礎(chǔ)的日常場(chǎng)景以及注意事項(xiàng)。
推薦下面兩個(gè)資料:
- Run Loops 官方資料:詳細(xì)介紹了 RunLoop 的基礎(chǔ)概念、運(yùn)行機(jī)制和使用方法,入門首選,在閱讀其他任何有關(guān) RunLoop 的文章遇到難以理解的概念時(shí),可以在這個(gè)官方資料里找到最原始、最準(zhǔn)確的解釋。建議把這個(gè)官方的資料讀上三遍,再動(dòng)手寫個(gè)代碼以加強(qiáng)理解。
- 深入理解RunLoop:這篇文章結(jié)合 RunLoop 的開源版本代碼給出了更多的實(shí)現(xiàn)細(xì)節(jié),并且討論了很多 RunLoop 的實(shí)際應(yīng)用。記得看下面的評(píng)論,知道了知識(shí)點(diǎn)不一定懂得怎么運(yùn)用以及解決問題,評(píng)論里有很多實(shí)際的問題可以幫助你更好地理解 RunLoop。
本文主要是記錄下自己的理解,并提供了些上面兩篇資料沒有顧及到的新內(nèi)容。
Q: RunLoop 與 Thread 的關(guān)系?
A: 了解 RunLoop 首先得了解 NSThread,開頭給出的官方資料其實(shí)是 Threading Programming Guide 中的一個(gè)章節(jié)。不必害怕 NSThread,它的易用性實(shí)際上和通常的并發(fā)首選 GCD 或者 NSOperation 差不多,只不過后兩者是對(duì) NSThread 的封裝,使用上更加方便,而且能夠避免大部分使用 NSThread 時(shí)的陷阱,關(guān)于這點(diǎn)可以看并發(fā)編程:API 及挑戰(zhàn)中的討論。
不管是 GCD, NSOperation,抑或是 NSThread 的底層 pthread,我們用多線程最終是為了運(yùn)行你的代碼而已,等任務(wù)代碼運(yùn)行完畢,這個(gè)線程就無法再次使用了,來看下面的例子:
// NSThread 還提供了兩個(gè)基于 selector 的方法,可以避免子類化。
class SEThread: Thread {
// 任務(wù)代碼放在 main() 方法里
override func main() {
// 打印 log 來說明當(dāng)前代碼是否運(yùn)行在主線程
NSLog("\(Thread.current) is main thread: \(Thread.isMainThread)")
}
}
func useAThread(){
let thread = SEThread.init()
// 啟動(dòng)線程,這個(gè)方法調(diào)用main()并維護(hù)相關(guān)狀態(tài),實(shí)際上調(diào)用start()
// 才會(huì)真正分配資源生成線程,文檔里明確告訴我們是這么做的。
thread.start()
}
讓一段代碼在 NSThread 子類對(duì)象里執(zhí)行,條件是很苛刻的:只有main()運(yùn)行在分配的線程里,而且必須通過start()啟動(dòng)該線程,這點(diǎn)可通過配合上面的 log 來驗(yàn)證,因此必須在main()里調(diào)用這段代碼。如果你維護(hù)一個(gè)上面線程的引用,在其他地方再次start(),應(yīng)用就直接 crash 了,唯一的提示是你嘗試再次啟動(dòng)這個(gè)線程,也就是說在線程的生命周期里只能啟動(dòng)一次。所以,NSThread 就是個(gè)一次性的資源。
那么有沒有這么一種機(jī)制,保留 NSThread 的資源,只在需要的時(shí)候使用呢?RunLoop 就是為此設(shè)計(jì)的。RunLoop 就是一個(gè)無限循環(huán),停留在main()里,通過添加 Sources(比如 NSTimer),姑且稱之為事件源,有事件發(fā)生時(shí)就喚醒線程(和通知機(jī)制類似)來干活(干活本質(zhì)上還是運(yùn)行一段代碼,這段代碼可以有多種來源),活干完了就讓線程休眠,然后 RunLoop 也進(jìn)入休眠狀態(tài),等待下個(gè)事件到來后喚醒線程干活,如此循環(huán),具體的處理流程在官方資料里有詳細(xì)的描述。我想了很多現(xiàn)實(shí)里的關(guān)系來比喻這兩者,但沒有很合適的。
總結(jié)下:NSThread 有兩種工作方式,一種是直接運(yùn)行任務(wù)代碼,比如上面的例子,比如通過 GCD 的 Block 提供任務(wù)代碼,是個(gè)一次性的資源,用完即廢;另一種就是搭配 RunLoop,需要處理事件時(shí)被 RunLoop 喚醒執(zhí)行代碼,執(zhí)行完畢后進(jìn)入休眠,按需使用,只要 RunLoop 還存在,線程就可以一直提供服務(wù)。
那么這兩種方式能不能結(jié)合起來呢,一舉兩得?寫個(gè)代碼測(cè)試下。
override func main() {
// NSTimer 的另外一種使用方法,timer 綁定的方法將會(huì)在當(dāng)前線程里按照設(shè)定的時(shí)間執(zhí)行
RunLoop.current.add(timer, forMode: .defaultRunLoopMode)
// 除了主線程的 RunLoop,其他線程的 RunLoop 必須手動(dòng)啟動(dòng)
RunLoop.current.run()
// 任務(wù)代碼,只是在當(dāng)前線程里循環(huán)1000000次。
for i in 0..<1000000 {
if i % 100000 == 0{
NSLog("Iterate to \(i)")
}
}
}
RunLoop 運(yùn)行后,代碼就一直停留在運(yùn)行這里無限循環(huán),正如其名,其實(shí)還可以指定超時(shí)時(shí)間,到達(dá)時(shí)間后自動(dòng)退出循環(huán)。如果 RunLoop 的事件源被移除或者結(jié)束后,RunLoop 無事可干,就會(huì)退出循環(huán)。在上面的代碼里,如果 timer 是重復(fù)執(zhí)行的,在 timer 結(jié)束前(可以使用invalidate()來停止),RunLoop 不會(huì)退出循環(huán),下面的任務(wù)代碼永遠(yuǎn)不會(huì)被執(zhí)行。所以,結(jié)合這兩種方式是沒有意義的:要么直接運(yùn)行一段任務(wù)代碼,結(jié)束后不再使用該線程;要么配合 RunLoop,有活干活,沒活休眠。
RunLoop 無法通過 init 的方式生成,只能通過 RunLoop 類的類變量獲?。?/p>
//這兩者在 Core Foundation 下都有對(duì)應(yīng)的方法: CFRunLoopGetCurrent(), CFRunLoopGetMain()
class var current: RunLoop
class var main: RunLoop
而只有在線程內(nèi)RunLoop.current獲取的才是那個(gè)線程的 RunLoop,線程的內(nèi)部環(huán)境有以下幾個(gè)入口:
NSThread 子類的
main()里,而且只有通過調(diào)用start()時(shí)main()內(nèi)部才是這個(gè)線程的環(huán)境,比如直接調(diào)用thread.main(),這時(shí)候main()內(nèi)部的線程環(huán)境是當(dāng)前線程,而不是 thread 本身的線程;NSThread 兩個(gè)基于 selector 的 API,調(diào)用的 selector 內(nèi)部;
-
GCD 和 NSOperationQueue 基于 Block 的 API,Block 里面。需要注意的是,GCD 里 sync 一類的方法出于優(yōu)化的目的,會(huì)盡可能在當(dāng)前線程里執(zhí)行,這時(shí)候 Block 里的線程環(huán)境很可能不是你想要的,比如:
DispatchQueue.global().sync { // 這里的線程環(huán)境很大可能是調(diào)用 DispatchQueue.global().sync{} 這條語(yǔ)句所在的線程 // 這一段代碼和直接寫成RunLoop.current.run()無異 RunLoop.current.run() }
NSThread 對(duì) RunLoop 是必需的,但 RunLoop 對(duì) NSThread 并不是必需的,除了主線程,其他線程并不會(huì)主動(dòng)生成 RunLoop,除非你通過RunLoop.current主動(dòng)索取,如果當(dāng)前線程沒有 RunLoop,這個(gè)方法會(huì)自動(dòng)生成一個(gè),在 NSThread 的生命周期內(nèi)用的是同一個(gè) RunLoop 對(duì)象。
Q: RunLoop mode 是什么?
A: 本來這似乎不是個(gè)問題,不過當(dāng)初看到深入理解RunLoop這篇文章的時(shí)候,每次看到講解 RunLoop mode 的時(shí)候我就暈了,但我這次直接看了官方資料的解釋后立馬就理解了,可能每個(gè)人對(duì)其他人的概念解釋的接受度不一樣,但幾乎每次看不懂中文版本的概念解釋時(shí),回頭看原始版本的解釋就明白了。如果你看這篇文章有概念不清楚的地方,去看官方資料。
RunLoop 和事件源的關(guān)系與通知機(jī)制類似,訂閱某些對(duì)象來獲取它們的動(dòng)態(tài),上一個(gè)問答里籠統(tǒng)提到的事件源被 RunLoop 大致分為兩類:

右邊的 Sources 在官方資料里有詳細(xì)的分類介紹,其中 Input sources 成分比較復(fù)雜,除了 performSelector,其他兩個(gè) Port source, Custom source 是一些比較底層的東西,暫時(shí)不懂也沒有關(guān)系(我不懂);而 Timer source 就是 NSTimer。
RunLoop 可以接受多個(gè) sources,不同場(chǎng)景下可能只需要接受某些事件源的信號(hào),怎么辦呢?RunLoop mode 就是為了解決這個(gè)問題的,將這些事件源任意組合,在運(yùn)行 RunLoop 時(shí)指定接受哪一個(gè)組合的信號(hào),這就是 mode 了,打個(gè)比方,RunLoop mode 就像早些年功能手機(jī)的模式:靜音模式,會(huì)議模式,等等。在具體的實(shí)現(xiàn)中,做法是先建立模式名稱,然后往模式里添加事件源。
// RunLoopMode 是一個(gè)結(jié)構(gòu)體,defaultRunLoopMode 是其一個(gè)預(yù)定義的 mode 名
// 這行代碼將 timer 添加到這個(gè) mode 下了
RunLoop.current.add(timer, forMode: .defaultRunLoopMode)
// run() 這個(gè)方法實(shí)際上是調(diào)用 run(mode:before:),這里的 mode 參數(shù)就是 .defaultRunLoopMode
// 從這行代碼開始進(jìn)入無限循環(huán),到了預(yù)定時(shí)間點(diǎn)就調(diào)用 timer 綁定的方法
RunLoop.current.run()
RunLoop mode 的設(shè)計(jì)對(duì)多個(gè)事件源進(jìn)行了分組隔離,但也帶來一個(gè)副作用:要想在某個(gè)模式下處理某個(gè)事件源,必須把這個(gè)事件源明確地添加到這個(gè)模式里,這有點(diǎn)繁瑣。為了解決這個(gè)小問題,引入了commonModes,被添加到這個(gè)模式的事件源,可以在所有其他的模式里使用,不必再手動(dòng)添加一次了。你可能見到這樣的技巧,讓添加到主線程的 NSTimer 在視圖滾動(dòng)的時(shí)候也能觸發(fā):
RunLoop.main.add(timer, forMode: .commonModes)
視圖滾動(dòng)時(shí)主線程的 RunLoop 會(huì)切換到UITrackingRunLoopMode,由于 timer 被添加到commonModes,其他任何模式下都會(huì)處理這個(gè) timer。
以上出現(xiàn)的三個(gè) mode: defaultRunLoopMode, UITrackingRunLoopMode, commonModes 就是 iOS 平臺(tái)的預(yù)定義模式。RunLoop 也支持自定義模式,雖然只在run(mode:before:)的文檔幾個(gè)詞里提到,還好深入理解RunLoop里明確指出了這點(diǎn),在run(mode:before:)直接使用新的自定義的 RunLoopMode 即可,就像 Dictionary 那樣,使用自定義模式的代碼如下:
let customMode = RunLoopMode.init("CustomMode")
RunLoop.current.add(timer, forMode: customMode)
// 這里指定了 RunLoop 的失效時(shí)間,而實(shí)際上 .distantFuture 也相當(dāng)于無窮時(shí)間了
RunLoop.current.run(mode: customMode, before: .distantFuture)
自定義模式也能使用添加到commonModes的所有事件源,但是需要處理下才能享受這個(gè)待遇:
//又是 CF 里的方法,使用起來相當(dāng)不方便;這行代碼讓 customMode 下也能處理所有添加到 commonModes 下的事件源。
//這行代碼放哪呢?只要在 RunLoop 運(yùn)行之前就行了。
CFRunLoopAddCommonMode(RunLoop.current.getCFRunLoop(), CFRunLoopMode.init("CustomMode" as CFString))
注意: 雖然添加到 commonModes 的事件源可以在所有其他的 mode 處理,但是 RunLoop 本身并不能在 commonModes 下運(yùn)行,RunLoop.current.run(mode: .commonModes, before: .distantFuture) 是無法啟動(dòng) RunLoop 的。文檔里沒提到這點(diǎn),還是蠻坑的。
RunLoop 如何切換 mode 呢?這部分看末尾的回答。
Q: 如何創(chuàng)建一個(gè)在后臺(tái)線程里運(yùn)行的 NSTimer?
A: 這篇文章的起因就是我要實(shí)現(xiàn)這個(gè)需求,當(dāng)初我是這樣達(dá)到同樣的目的的:直接將 timer 添加到主線程里,然后在綁定的方法里使用 GCD 切換到后臺(tái)線程。為了比較兩種方法里的性能差異,我用下面的方法實(shí)現(xiàn)了一個(gè)直接在后臺(tái)線程里運(yùn)行的 NSTimer。
DispatchQueue.global().async {
// sheduledTimer 這個(gè)方法將創(chuàng)建的 NSTimer 添加到當(dāng)前線程的 RunLoop 的 defaultMode 下
Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerFire), userInfo: nil, repeats: true)
}
NSTimer 無法觸發(fā),即使在代碼里手動(dòng)觸發(fā),也只有一次執(zhí)行。原因在哪里呢?現(xiàn)在我們知道因?yàn)檫@個(gè)線程的 RunLoop 沒有運(yùn)行,再添加一行代碼即可:
RunLoop.current.run(mode: .defaultRunLoopMode, before: Date.distantFuture)
然而這樣的代碼還有一個(gè)問題:無法停止那個(gè) timer,RunLoop 不會(huì)退出循環(huán),進(jìn)而這個(gè)線程永遠(yuǎn)不會(huì)消失。所以我們需要維持一個(gè)對(duì) timer 的引用,并且合適的時(shí)間停止它,這樣 RunLoop 會(huì)退出循環(huán),進(jìn)而線程也會(huì)被回收銷毀,另外,由于 timer 會(huì)保持對(duì) target 的強(qiáng)引用,為了避免引用循環(huán),target 里應(yīng)該維持一個(gè)弱引用。你可以給線程一個(gè)名字,在 timer 的 selector 里添加斷點(diǎn)來觀察這個(gè)線程的調(diào)用幀以及它的銷毀。
不過當(dāng)初的解決過程并不是這樣的,我搜索到了這個(gè)頁(yè)面: Run repeating NSTimer with GCD?,一個(gè)近6年前的問題,下面唯一的高票答案雖然本身提供了一些替代方向,不過他最大的錯(cuò)誤是認(rèn)為 timer 無法觸發(fā)的原因是因?yàn)?GCD 派發(fā)的 thread 沒有 RunLoop,完全是胡說八道,并且他對(duì)下面另外一個(gè)比較接近的答案(添加了RunLoop.current.run())給出了錯(cuò)誤的指導(dǎo)意見,讓這個(gè)答案止步于此。我起初看到這個(gè)頁(yè)面時(shí)沒有死心,嘗試用 NSThread 去解決這個(gè)問題,無意中成功了,下面是使用 Thread 的實(shí)現(xiàn):
func configurateTimerInBackgroundThread(){
let thread = Thread.init(target: self, selector: #selector(addTimer), object: nil)
//這里有個(gè)問題,這個(gè)方法返回后,thread會(huì)被回收銷毀嗎?
//不會(huì),除非 RunLoop 退出循環(huán),這里的達(dá)成條件是停止timer。
//查看內(nèi)存引用圖,發(fā)現(xiàn)Foundation框架本身會(huì)持有新的線程對(duì)象,當(dāng)然
//在RunLoop退出或者沒有RunLoop運(yùn)行的時(shí)候,會(huì)及時(shí)斷開引用,
//以便對(duì)象被回收銷毀,不過,上面提到有時(shí)候在模擬器里不起作用。
thread.start()
}
@objc func addTimer() {
weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerFire), userInfo: nil, repeats: true)
// 這行關(guān)鍵代碼不知道怎么加上去的,那個(gè)頁(yè)面下接近的那個(gè)答案我看到投票是為-1就略過了
RunLoop.current.run()
}
既然 NSThread 可以,GCD 沒道理不行。抱著這個(gè)疑惑,我最終決定還是好好學(xué)習(xí)下 RunLoop 的官方資料,于是問題解決了。所有的 NSThread 都有對(duì)應(yīng)的 RunLoop(官方資料開頭原文是:each thread, including the application’s main thread, has an associated run loop object)。即使是高票答案也不能全信,即使是一個(gè)多年經(jīng)驗(yàn)的工程師,不保持學(xué)習(xí)是不行的。如果我的文章里有任何錯(cuò)誤的地方,請(qǐng)指出來。
解決了這個(gè)問題后,我想起來直接搜索這個(gè)問題,然后找到了這個(gè): How do I create a NSTimer on a background thread?,同樣古老的問題,可以看到多個(gè)基于 GCD 的 答案(有個(gè)大神也現(xiàn)身了),有些東西我從來沒用過,比如 GCD 版本的 timer。如果你是寫 Swift 的,看這些答案會(huì)很痛苦,因?yàn)?GCD 的 API 在 Swift 里有很多版本,而且很多根本就沒文檔。
Q: 如何創(chuàng)建一個(gè)常駐后臺(tái)的線程?
A: 這是一個(gè)很常見的的問題,幾乎所有關(guān)于 RunLoop 的文章都會(huì)拿 AFNetworking 早期版本的代碼做范例。來分析下這個(gè)需求,要求線程常駐后臺(tái)以便響應(yīng)事件,這個(gè)需求嘛,從 RunLoop 的設(shè)計(jì)來看,線程的設(shè)計(jì)目標(biāo)之一就是隨時(shí)響應(yīng)事件?,F(xiàn)在來討論下怎樣實(shí)現(xiàn)是最合適的?
要求線程不退出,那就是保持 RunLoop 不退出循環(huán),讓 RunLoop 不退出循環(huán)就要求當(dāng)前運(yùn)行的模式下持有有效的 sources: Input sources 或者 Timer sources,其中 NSTimer 會(huì)定期喚醒線程執(zhí)行綁定的方法,其實(shí)讓線程一直保持休眠是最好不過了,只要 source 不觸發(fā)事件,線程和 RunLoop 就會(huì)一直休眠下去,直到被 source 喚醒,那么 Input sources 是比較合適的,Input sources 有三類,其中最合適的是 Port-Based Sources,它的代碼最少,AFNetworking 的代碼就是這樣做的,用 Swift 寫就是下面這樣:
// 添加一個(gè) mach port,但啥也不做,RunLoop 會(huì)一直等待 mach port 發(fā)送消息
RunLoop.current.add(NSMachPort.init(), forMode: .defaultRunLoopMode)
RunLoop.current.run(mode: .defaultRunLoopMode, before: .distantFuture)
而另一個(gè) Custom Input Sources 則要多兩行代碼,而且得用 Core Foundation 框架,示例如下:
// 像上面的 mach port 一樣,只是添加一個(gè)占位 input source
var sourceContext = CFRunLoopSourceContext.init()
let customSource = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &sourceContext)
CFRunLoopAddSource(RunLoop.current.getCFRunLoop(), customSource, CFRunLoopMode.defaultMode)
RunLoop.current.run(mode: .defaultRunLoopMode, before: .distantFuture)
深入理解RunLoop 在講解這個(gè)案例時(shí)提到:
RunLoop 啟動(dòng)前內(nèi)部必須要有至少一個(gè) Timer/Observer/Source
Observer 是 RunLoop Observer,看名字就知道是干什么的了,它能跟蹤 RunLoop 的運(yùn)行狀態(tài),在 Run Loop Observers 里有詳細(xì)的描述,也有使用范例。實(shí)際上,Observer 并不能維持 RunLoop 的運(yùn)行,它只是用來跟蹤 RunLoop 的狀態(tài),官方資料在 Configuring the Run Loop 是這樣介紹 RunLoop 的運(yùn)行條件的:
Before you run a run loop on a secondary thread, you must add at least one input source or timer to it. If a run loop does not have any sources to monitor, it exits immediately when you try to run it.
寫個(gè)代碼測(cè)試下就知道了,在 RunLoop 運(yùn)行前只添加一個(gè) Observer,運(yùn)行 RunLoop 后根本不能觀察到相關(guān)狀態(tài),因?yàn)?RunLoop 根本就沒有啟動(dòng),run(mode:before:)會(huì)返回一個(gè) Bool 值來表示 RunLoop 是否啟動(dòng)成功,用這個(gè)方法來啟動(dòng) RunLoop 就知道結(jié)果了。
上面沒有提到線程從哪兒獲取,NSThread, GCD, NSOperationQueue? 后兩者后基于某些規(guī)則維護(hù)著一個(gè)線程池,提交的 Block 不一定能及時(shí)分配到線程,使用 NSThread 就沒問題了(當(dāng)然這時(shí)候系統(tǒng)本身的資源很可能就不足了,這又是另外一個(gè)問題了);另外,日后若是想給這個(gè)提交 Block 的線程發(fā)送個(gè)任務(wù),需要我們?yōu)檫@個(gè)線程維護(hù)一個(gè)引用(強(qiáng)行找理由)。如果是需要這個(gè)線程單獨(dú)處理任務(wù),還是使用 NSThread 比較好。
iOS 似乎不鼓勵(lì)使用NSMachPort,它傳遞的消息NSPortMessage類只在 macOS 上是公開的;而NSMachPort的替代者NSMessagePort的文檔里又建議避免使用,這個(gè)類基本上也廢棄了。在 RunLoop 響應(yīng)事件的流程里,Custom Input Sources 優(yōu)先于 Port-Based Sources,而且在 iOS 里也有直接的應(yīng)用,比如接下來要講到的 NSObject 的 performSelector 系列方法。Custom Input Sources 本身我還沒有深入了解,不懂。
Q: NSObject 的 performSelector 系列方法...
A: 本來假裝寫成"如何實(shí)現(xiàn)的?",但發(fā)現(xiàn)這并不是一個(gè)好問題,就是用 RunLoop 相關(guān)的技術(shù)實(shí)現(xiàn)的嘛,像 afterDelay 的兩個(gè)方法,文檔里就直接指明使用了 timer。不過隨手使用了幾個(gè)方法后,發(fā)現(xiàn)這個(gè)系列的方法有很多需要注意的地方。在這系列方法里你可以看到很多深入理解RunLoop里提到的有關(guān)代碼層次的細(xì)節(jié)(可通過在 selector 里設(shè)置斷點(diǎn)來查看幀棧),借此驗(yàn)證下也可以幫助你更好地理解深入理解RunLoop這篇文章,這些細(xì)節(jié)當(dāng)然在上面的內(nèi)容也有,不過放到上面內(nèi)容就太繁雜了。
官方資料將 Input sources 細(xì)分成三個(gè)類別:
- Port-Based Sources: 對(duì)應(yīng)深入理解RunLoop里的 Source1,回調(diào)函數(shù)為
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ - Custom Input Sources: 對(duì)應(yīng) Source0,回調(diào)函數(shù)為
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ - Cocoa Perform Selector Sources:就是 NSObject 的 performSelector 系列。
如下,帶 modes 參數(shù)的可以指定運(yùn)行模式,沒帶的則在.defaultRunLoopMode模式下運(yùn)行:
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
這里面 afterDelay 的方法基于 Timer,可以算是 Timer sources 一類,而其他的方法基于 Source0,估計(jì)官方也不好分類,就把它擱 Input sources 里了。這個(gè)系列還有個(gè)performSelectorInBackground:withObject:,這個(gè)方法并沒有被 RunLoop 官方資料歸納到這部分,查看調(diào)用幀棧發(fā)現(xiàn)這個(gè)方法的確沒有用到 RunLoop。
現(xiàn)在我們知道這些方法生效的首要前提當(dāng)然是線程的 RunLoop 在運(yùn)行,而且是以方法指定的模式運(yùn)行,然而線程的 RunLoop 你不主動(dòng)獲取根本不會(huì)生成,主線程還好,它的 RunLoop 在應(yīng)用啟動(dòng)過程中就激活了,而從線程外部是無法獲取它的 RunLoop 的,那么有兩種用法:一是使用 GCD 或者 NSOperation 中帶有 Block 的 API,這樣就直接獲得了線程所在的環(huán)境,由于這些 performSelector 方法本身會(huì)給線程添加一個(gè) source,剩下的只需要讓 RunLoop 運(yùn)行即可;二是預(yù)先讓線程的 RunLoop 運(yùn)行,使用 NSThread 基于 selector 的 API 或者子類化,像 AFNetworking 那樣添加一個(gè) mach port 讓 RunLoop 不退出。
onThread 的方法有一個(gè)副作用需要特別注意:排他性。如果 RunLoop 在 onThread 方法指定的模式下運(yùn)行,它會(huì)移除 RunLoop 在這個(gè)模式下的所有事件源,主線程除外。這到底是個(gè) Bug 還是個(gè) Feature(確保添加的 selector 能夠得到執(zhí)行,我瞎猜的)不得而知,按說這么重要的事情,文檔里應(yīng)該指出來,居然沒有,那這很可能是個(gè) Bug。另外,如果你在自定義模式下運(yùn)行 RunLoop,并且將這個(gè)自定義模式通過CFRunLoopAddCommonMode(_:_:)將其加入.commonModes,performSelector:onThread:withObject:waitUntilDone:也會(huì)生效,并且移除添加到commonModes的事件源,不過這種情況下使用帶modes參數(shù)的API并指定.defaultRunLoopMode的同等代碼沒有這樣的效果。
于是使用 onThread 方法時(shí),當(dāng) selector 執(zhí)行完畢后,RunLoop 里已經(jīng)沒有事件源了,直接退出循環(huán);一次性執(zhí)行多個(gè) onThread 方法則會(huì)在全部執(zhí)行完畢后退出 RunLoop。
深入理解RunLoop 在分析 AFNetworking 的建立一個(gè)常駐后臺(tái)的線程的做法時(shí)指出:
當(dāng)需要這個(gè)后臺(tái)線程執(zhí)行任務(wù)時(shí),AFNetworking 通過調(diào)用 [NSObject performSelector:onThread:..] 將這個(gè)任務(wù)扔到了后臺(tái)線程的 RunLoop 中。
由于 onThread 方法的特性,這個(gè)辦法其實(shí)只能奏效一次。那么有沒有辦法可以多次在同一個(gè)線程對(duì)象上執(zhí)行這個(gè)方法呢?可以的,讓線程的 RunLoop 重新啟動(dòng)就可以了,但是 RunLoop 只能從線程的內(nèi)部獲取,怎么解決呢?我們可以利用 RunLoop Observer 為線程實(shí)現(xiàn) RunLoop 自啟動(dòng)的功能,參考下一個(gè)回答。
Q: 如何切換 RunLoop 的 mode?如何重啟 RunLoop?
A: 其實(shí)后一個(gè)問題是前一個(gè)問題的子集。
RunLoop 的文檔和開頭給出的官方資料都沒有直接提到如何切換 RunLoop 的 mode。一個(gè)正常的想法是:先退出當(dāng)前模式,然后在run(mode:before:)里重新選擇模式開始新的循環(huán)。
如何退出 RunLoop 呢?(退出后不再處理這個(gè)模式下的事件)
在
run(mode:before:)里可以指定失效時(shí)間,到了指定的時(shí)間 RunLoop 會(huì)自動(dòng)退出循環(huán)。RunLoop 運(yùn)行后會(huì)檢查當(dāng)前運(yùn)行模式下的 Input sources 和 Timer sources,如果 Input sources 的數(shù)量為0, Timer sources 里有效的 timer 數(shù)量也為0,RunLoop 就會(huì)退出循環(huán),在上面這個(gè)簡(jiǎn)單的例子里,只要
timer.invalidate()即可。不過文檔指出,移除你已知的事件源并不能保證 RunLoop 退出循環(huán),在 macOS 平臺(tái)下,出于某些原因,系統(tǒng)會(huì)自動(dòng)添加某些事件源,但沒有明確指出其他平臺(tái)會(huì)不會(huì)這樣做。我在Demo里測(cè)試時(shí)發(fā)現(xiàn)在多個(gè)模擬器上這樣做無法讓 RunLoop 退出循環(huán),而在真機(jī)上沒有問題,但是呢,同樣的手法在我其他項(xiàng)目的模擬器上又沒有問題。-
還可以強(qiáng)制退出 RunLoop,需要使用 Core Foundation 里的方法:
CFRunLoopStop(RunLoop.current.getCFRunLoop())RunLoop 是 CFRunLoopRef 的封裝,但是有不少相關(guān)的方法沒有封裝到 RunLoop 類里,必須使用 CF 框架里的方法,很不方便。
注意:NSThread 在其生命周期內(nèi)使用的 RunLoop 都是同一個(gè)對(duì)象,退出后通過RunLoop.current獲取的 RunLoop 并不是重新生成的新對(duì)象。
從代碼上看,運(yùn)行 RunLoop 應(yīng)該是 NSThread 線程入口比如main()里的最后一行代碼,不然后面的代碼就沒什么意義了。那切換模式的代碼在哪里處理呢?
這里就要用到 RunLoop Observer 了,RunLoop 本身并沒有提供接口查詢它的狀態(tài),只能通過添加 observer 來跟蹤它的狀態(tài)。RunLoop Observer 的唯一版本是 Core Foundation 里的 CFRunLoopObserver,有兩個(gè)方法可以創(chuàng)建 Observer:
CFRunLoopObserverCreateWithHandler
CFRunLoopObserverCreate
建議使用帶閉包的方法,另外一個(gè)方法里捕獲變量非常曲折。
// 放在 main() 里,RunLoop 運(yùn)行之前
func addObserver(){
let observer = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault, //不懂,默認(rèn)參數(shù)就好
CFRunLoopActivity.allActivities.rawValue, //這個(gè)值表示觀察所有的狀態(tài),由于類型不兼容,只能使用原始值
true, //這個(gè)參數(shù)和NSTimer的repeats 參數(shù)一樣,
0 // 優(yōu)先級(jí),如果有多個(gè)observer,這個(gè)值越小優(yōu)先級(jí)越高)
{ (observer, activity) in
switch activity {
case .entry://進(jìn)入了RunLoop,開始循環(huán)檢查
case .beforeTimers://檢查timer
case .beforeSources://檢查input source,有事件就處理
//如果input sources里沒有事件發(fā)生并且timer的觸發(fā)點(diǎn)還沒到來,進(jìn)入休眠
//或者input sources的事件處理完畢并且timer的觸發(fā)點(diǎn)還沒到來,進(jìn)入休眠
case .beforeWaiting:
//被喚醒,準(zhǔn)備處理觸發(fā)喚醒的事件,比如timer的觸發(fā)時(shí)間點(diǎn)到了,
//或者input sources發(fā)來了信號(hào)。如果是input sources,從頭開始檢查一遍
case .afterWaiting:
case .exit://已退出 RunLoop
/.添加 sources,重新(選擇模式)啟動(dòng) RunLoop./
RunLoop.current.run(mode: aMode, before: aDate)
default: break
}
}
//Observer需要指定要觀察的模式,RunLoop在這個(gè)模式下運(yùn)行時(shí),發(fā)送以上通知給這個(gè)observer
//如果想跟蹤所有模式,就指定commonModes
CFRunLoopAddObserver(RunLoop.current.getCFRunLoop(), observer, CFRunLoopMode.defaultMode)
}
我建議你添加一個(gè) observer 觀察 RunLoop 來驗(yàn)證上面的狀態(tài)變化,特別是主線程的 RunLoop,你可以看到視圖開始滾動(dòng)以及停止時(shí),RunLoop 會(huì)退出然后重新進(jìn)入新的模式(這就是重啟了)。通過實(shí)際觀察,你會(huì)發(fā)現(xiàn)官方資料里有些地方寫的不是那么完善,比如beforeTimers這個(gè)通知,下面是最清楚的一條解釋:
When the run loop is about to process a timer.
這個(gè)描述看上去是 timer 的觸發(fā)時(shí)間點(diǎn)到了,RunLoop 要開始處理了,「深入理解RunLoop」在 RunLoop 的內(nèi)部邏輯對(duì) CFRunLoop 的源碼進(jìn)行了解讀,也沿用了這個(gè)解釋;我這里將其解釋為檢查timer,做個(gè)測(cè)試,添加一個(gè) mach port 讓 RunLoop 不退出,會(huì)發(fā)現(xiàn)不管有沒有 timer,RunLoop 都會(huì)給 observer 發(fā)送beforeTimers通知,回頭再來看RunLoop 的內(nèi)部邏輯里這個(gè)簡(jiǎn)化版本的代碼:
/// 2. 通知 Observers: RunLoop 即將觸發(fā) Timer 回調(diào)。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 8. 通知 Observers: RunLoop 的線程剛剛被喚醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 9.1 如果一個(gè) Timer 到時(shí)間了,觸發(fā)這個(gè)Timer的回調(diào)。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
所以我將.beforeTimers解釋為檢查 timer 是說得過去的,.beforeSources通知同理。
RunLoop 的處理流程可以這樣簡(jiǎn)單理解:進(jìn)入 RunLoop 后,開始循環(huán),首先檢查 timer(發(fā)送.beforeTimers),然后再檢查 input sources(發(fā)送beforeSources),如果inpt sources有事件發(fā)生,就處理,接下來就進(jìn)入休眠(發(fā)送beforeWaiting)。之后如果有 timer 到了觸發(fā)點(diǎn),RunLoop 被喚醒(發(fā)送afterWaiting),執(zhí)行 timer 綁定的方法,然后重新開始循環(huán),檢查 timer,檢查 input source,沒事做就休眠等待信號(hào)。那么 input sources 怎么處理的呢?因?yàn)?input sources 的信號(hào)隨時(shí)都可能來,來了之后,如果 RunLoop 在休眠,喚醒(發(fā)送afterWaiting),又開始從頭檢查,檢查 timer,檢查 input sources,誒,有事情,處理,處理完了休眠。
實(shí)際上,不退出 RunLoop 直接使用run(mode:before:)里選擇新的模式運(yùn)行也是可以的。官方資料在 Starting the Run Loop 里提到 RunLoop 是可以遞歸運(yùn)行的,也就是說 RunLoop 里可以再嵌套一個(gè) RunLoop:

像上面這種正常的嵌套會(huì)有以下影響:
-
.exit通知會(huì)出現(xiàn)重復(fù)現(xiàn)象,如果依賴.exit通知,注意過濾重復(fù)的通知; - 雖然將 observer 添加到
commonModes能讓 observer 觀察所有模式,但是在這種 RunLoop 嵌套的情況下,還是必須再添加一次;
如果你不按上面的套路出牌,比如在兩種模式中來回嵌套,或是同一種模式里嵌套自身,在某些條件下的確是會(huì)出問題的。