寫在前面
閱讀 Apple Deleloper Run Loops 的筆記。
簡介
Run Loops 是和線程息息相關(guān)的基礎(chǔ)組件。
顧名思義,它就是一個循環(huán),目的是:當(dāng)有工作時,讓線程忙碌起來;當(dāng)沒有工作時,讓線程休眠。
Run loop 的管理不是完全自動化的,開發(fā)者仍然必須設(shè)計(jì)線程相關(guān)代碼,讓 run loop 在合適時間開啟。
Cocoa 和 Core Foundation 都提供了 run loop 對象輔助配置和管理。
開發(fā)者不需要再創(chuàng)建這些對象。每個線程(包括主線程)都有一個 run loop 對象與之關(guān)聯(lián)。
App 在啟動時,會在主線程上設(shè)置和啟動 run loop。
只有次要線程需要手動啟動 run loop。

如圖所示,run loop 接收2種類型數(shù)據(jù):
- Input sources : 傳遞異步事件,經(jīng)常是來自其他線程或不同應(yīng)用的信息。
- Timer sources : 傳遞同步事件,定時或重復(fù)發(fā)生。
除了處理資源,run loops 也創(chuàng)建了關(guān)于 run loop 行為的通知。
注冊一個 run loop observer 可以接受到這些通知,并且可以在線程上使用它們。
可以使用 Core Foundation 來安裝 run loop observer 到線程上。
Run Loop Modes
一個 run loop mode 是 input sources 和 timer sources 的集合。
傳輸過程中,只有關(guān)聯(lián)到 mode 的 source 才允許傳遞事件。
Cocoa 和 Core Foundation 都定義了默認(rèn) mode 和一些常用 mode。
開發(fā)者也可以自定義 mode。
使用 mode 可以過濾事件。
預(yù)置的 mode
Default
NSConnectionReplyMode(Cocoa)
kCFRunLoopDefaultMode(Core Foundation)
默認(rèn) mode ,在大部分操作中使用,大多數(shù)時候,應(yīng)該使用這個 mode 去開啟 run loop 和配置 input source。Connection
NSConnectionReplyMode(Cocoa)
Cocoa 使用這個 mode,結(jié)合 NSConnection 對象去管理回復(fù),開發(fā)者很少需要使用它。Modal
NSModalPanelRunLoopMode(Cocoa)
Cocoa 使用這個 mode 去標(biāo)記模態(tài)面板。Event tracking
NSEventTrackingRunLoopMode(Cocoa)
當(dāng)用戶接口追蹤循環(huán)過程中,如鼠標(biāo)拖拽,Cocoa 使用這個 mode 去限制即將到來的事件。Common modes
NSRunLoopCommonModes(Cocoa)
kCFRunLoopCommonModes(Core Foundation)
這是一個可以配置的組合,它已經(jīng)包含了常用 mode ,開發(fā)者也可以自己添加自定義 mode 。
Input Sources
Input source 異步地傳遞事件給線程。
事件資源依賴于 input source 的類型,一般分為2類:
- port_based 資源:管理應(yīng)用的 Mach ports。
- 自定義 input source:管理自定義事件資源。
無須關(guān)心 run loop 的類型,系統(tǒng)一般會實(shí)現(xiàn)2種類型。
這2種資源唯一的區(qū)別在于它們是如何被發(fā)信號的:
port_based 資源由內(nèi)核自動發(fā)信號,而自定義資源必須手動由其他線程完成。
如果某個 input source 不是當(dāng)前運(yùn)行中的 mode ,它創(chuàng)建的任何事件都會被掛起,直到 run loop 運(yùn)行到合適的 mode 。
Port_Based Sources
Cocoa 和 Core Foundation 內(nèi)置創(chuàng)建這類 source 的支持。
比如說,開發(fā)者從不需要直接創(chuàng)建一個 input source ,只需要創(chuàng)建一個端口對象,然后使用 NSPort 的方法,添加端口到 run loop。端口對象會創(chuàng)建并配置好需要的 input source。
在 Core Foundation 中,你必須手動創(chuàng)建端口和它的 run loop 資源。
需要使用關(guān)聯(lián)特定端口類型(CFMachPortRef, CFMessagePortRef, CFSocketRef)的方法來創(chuàng)建合適的對象。
Custom Input Sources
使用 Core Foundation 中關(guān)聯(lián)了 CFRunLoopSourceRef 類型的方法,來創(chuàng)建 custom input source,并通過很多回調(diào)方法來配置它。
為了定義 custom input source 的行為,必須定制事件傳遞機(jī)制。
Cocoa Perform Selector Sources
除了 port_based sources,Cocoa 定義了能在任何線程上 perform selector 的 input source。
跟 port_based sources 一樣,perform selector 請求會在目標(biāo)線程上排好序。
不同的是,它在執(zhí)行后,需要移除自身。
Timer Sources
Timer sources 在預(yù)設(shè)的時間點(diǎn),同步傳遞事件給線程。
Timer 不是一個實(shí)時機(jī)制,跟 input sources 一樣,它與 run loop 中特定的 modes 關(guān)聯(lián),如果不在 run loop 當(dāng)前的 modes 中,那么它不會被執(zhí)行。
Run Loop observers
Run Loop observers 事件類型:
- The entrance to the run loop.
- When the run loop is about to process a timer.
- When the run loop is about to process an input source.
- When the run loop is about to go to sleep.
- When the run loop has woken up, but before it has processed the event that woke it up.
- The exit from the run loop.
Run Loop 事件順序:
- Notify observers that the run loop has been entered.
- Notify observers that any ready timers are about to fire.
- Notify observers that any input sources that are not port based are about to fire.
- Fire any non-port-based input sources that are ready to fire.
- If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.
- Notify observers that the thread is about to sleep.
- Put the thread to sleep until one of the following events occurs:
- An event arrives for a port-based input source.
- A timer fires.
- The timeout value set for the run loop expires.
- The run loop is explicitly woken up.
- Notify observers that the thread just woke up.
- Process the pending event.
- If a user-defined timer fired, process the timer event and restart the - loop. Go to step 2.
- If an input source fired, deliver the event.
- If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.
- Notify observers that the run loop has exited.
因?yàn)?timer 和 input sources 的監(jiān)聽通知會在這些事件實(shí)際發(fā)生前被傳遞。
所以,如果時間要求比較苛刻的話,可使用 sleep 和 awake-from-sleep 通知來確認(rèn)時間。
何時需要使用 Run Loop?
只有創(chuàng)建次要線程時,才需要顯式運(yùn)行一個 run loop。
主線程上的 run loop 是至關(guān)重要的。
因此,app frameworks 提供了運(yùn)行 main application loop 的代碼,并且自動啟動 run loop。
在以下情況,你需要去啟動一個 run loop
- Use ports or custom input sources to communicate with other threads.
- Use timers on the thread.
- Use any of the performSelector… methods in a Cocoa application.
- Keep the thread around to perform periodic tasks.
使用 Run Loop 對象
一個 run loop 對象提供添加 input sources, timers, run-loop observers,并運(yùn)行它們的主要接口,每個線程都有一個關(guān)聯(lián)的 run loop 對象。Cocoa 中,它是 NSRunLoop 的實(shí)例,更底層的,它是一個指向 CFRunLoopRef opaque type 的指針。
獲取 Run Loop 對象
方法:
- Cocoa 應(yīng)用,使用 NSRunLoop 的
currentRunLoop。 - 使用 CFRunLoopGetCurrent 方法。
雖然它們不是無縫連接的類型,但可以通過 NSRunLoop 里的 getCFRunLoop 方法來獲得一個 CFRunLoopRef opaque type,因?yàn)樗鼈兌际且猛?run loop,所以可以隨意使用其中之一。
配置 Run Loop
運(yùn)行一個 run loop 前,它必須至少添加了一個 input source 或 timer,否則無法運(yùn)行它。
除了 sources,還可以安裝 run loop observers,然后使用它們來判斷 run loop 的運(yùn)行狀態(tài)。
當(dāng)配置一個長時間存活的 run loop,最好得添加至少一個 input source 來接收信息。因?yàn)榧词固砑右粋€ timer 也可以運(yùn)行 run loop,但一旦 timer 結(jié)束,這種方式也就失效了,這樣就會造成 run loop 退出。
啟動 Run Loop
只需要在次要線程中啟動 Run Loop,有以下方式啟動 run loop
Unconditionally
最簡單方式,可以添加或移除 input sources, timers,但只能通過 kill 來停止它,而且也無法運(yùn)行在“自定義 mode”。With a set time limit
比 Unconditionally 更好的方式是,運(yùn)行一個帶有 timeout 變量的 run loop。In a particular mode
除了 timeout 變量,可以使用特定 Mode 運(yùn)行。
退出 Run Loop
方式
- Configure the run loop to run with a timeout value.
比較推薦的方式,在退出之前,run loop 處理完該處理的。 - Tell the run loop to stop.
調(diào)用CFRunLoopStop方法也能起到使用 timeout 變量同樣的效果,不同之處在于,可以在Unconditionally啟動的 run loops 上使用這項(xiàng)技術(shù)。
雖然移除 input sources 和 timers 也有可能會造成 run loop 退出,但這不是停止 run loop 的可靠方式。系統(tǒng)在需要處理事件時,會添加 input sources 到 run loop 中,因?yàn)殚_發(fā)者無法知道這些 input sources,所以無法移除它們,這就無法使 run loop 退出。
線程安全和 Run Loop 對象
是否安全取決于使用什么 API 來操作 run loop。
Core Foundation 中的方法一般是線程安全的,可在任何線程中調(diào)用,但最好還是在持有 run loop 的線程上調(diào)用。
Cocoa NSRunLoop 不是線程安全的,使用時,要盡量在同一持有 run loop 的線程中使用。
總結(jié)
Run Loop 是一個特殊的循環(huán):有工作時,讓線程忙碌;當(dāng)沒有工作時,讓線程休眠。
它接收的數(shù)據(jù)類型:
- Input Sources
- Port-Based Input Sources
- Custom Input Sources
- Cocoa Perform Selector Sources
- Timer Sources
不同的 Input Sources 和 Timer Sources 的集合就是 Run Loop Modes。
除了 Sources 外,還能注冊 Run Loop Observer 來接收 Run Loop 行為的通知。
只有在次級線程上,才需要使用 Run Loop 對象來管理 Run Loop。