引用
Apple文檔 -- Run Loops
深入理解RunLoop
iOS刨根問(wèn)底-深入理解RunLoop
首先感謝ibireme及KenshinCui兩位大神對(duì)Run Loops做出的探討和總結(jié). 本文將大量地直接或間接引用自以上三個(gè)出處的內(nèi)容, 是對(duì)這三篇文章的一個(gè)理解和整合. 如需更加深入了解Run Loops, 還請(qǐng)單擊上文引用鏈接.
目錄
了解Run Loops
- 概念
- Run Loops與線程的關(guān)系
- CFRunLoop相關(guān)類
- Call out
- 運(yùn)行流程
- 底層實(shí)現(xiàn)
- 什么時(shí)候使用
應(yīng)用
- NSTimer
- AutoreleasePool
- UI更新
- NSURLConnection
了解Run Loops
一般來(lái)說(shuō), 我們開辟一個(gè)線程, 在任務(wù)執(zhí)行完之后就會(huì)退出. 如果我們想反復(fù)執(zhí)行線程里的這些任務(wù), 可以使用 for / while / do while
等各種循環(huán), 例如:
int loop()
{
initialize();
while (1) {
// do something
if (quit) break;
sleep(0.2);
}
return 0;
}
雖然這樣簡(jiǎn)單方便, 但會(huì)有幾個(gè)問(wèn)題:
- 如何管理事件/消息
- 如何讓線程在沒(méi)有處理消息時(shí)休眠以避免資源占用
- 如何在有消息到來(lái)時(shí)立刻被喚醒
使用Run Loops能夠解決這些問(wèn)題.
概念
Run Loops (運(yùn)行循環(huán)) 是與線程相關(guān)聯(lián)的基礎(chǔ)設(shè)施的一部分, 目的是在有工作要做時(shí)讓線程忙, 而在沒(méi)有工作時(shí)讓線程進(jìn)入睡眠狀態(tài).
iOS中的 Run Loops 指的分別是 Cocoa 中的 NSRunLoop
以及 CoreFoundation 中的 CFRunLoopRef
. 其中, CFRunloopRef是純C的函數(shù), 而NSRunloop僅僅是CFRunloopRef的OC封裝, 并未提供額外的其他功能, 因此本文將直接討論CF框架下的CFRunLoopRef.
NSRunLoop一般不被認(rèn)為是線程安全的, 并且它的方法只應(yīng)在當(dāng)前線程的上下文中被調(diào)用. 您永遠(yuǎn)不要嘗試在不同線程中調(diào)用同一個(gè)NSRunLoop對(duì)象的方法焦履,因?yàn)檫@樣做可能會(huì)導(dǎo)致意外結(jié)果禁添。
Run Loops與線程關(guān)系
Run Loops是用來(lái)管理線程的, 包括線程的 循環(huán)運(yùn)行 / 事件處理 / 喚醒 等等, 因此我們不能拋開線程來(lái)談Run Loops.
- Run Loops與線程是一一對(duì)應(yīng)的, 其關(guān)系是保存在一個(gè)全局的 Dictionary 里.
- 我們不能主動(dòng)創(chuàng)建RunLoop, 只能通過(guò)CFRunLoopGetMain() 和 CFRunLoopGetCurrent()分別獲取主線程和子線程的RunLoop.
- 主線程中, 系統(tǒng)會(huì)自動(dòng)幫我們創(chuàng)建一個(gè)RunLoop; 而子線程中的RunLoop采用類似懶加載的機(jī)制, 即我們第一次去獲取的時(shí)候才會(huì)創(chuàng)建, 如果不調(diào)用CFRunLoopGetCurrent()獲取RunLoop, 那么就一直沒(méi)有.
這兩個(gè)函數(shù)內(nèi)部的邏輯大概是下面這樣 (CFRunloop開源):
/// 全局的Dictionary症杏,key 是 pthread_t豫尽, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問(wèn) loopsDic 時(shí)的鎖
static CFSpinLock_t loopsLock;
/// 獲取一個(gè) pthread 對(duì)應(yīng)的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次進(jìn)入時(shí)亚斋,初始化全局Dic作媚,并先為主線程創(chuàng)建一個(gè) RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接從 Dictionary 里獲取帅刊。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到時(shí)纸泡,創(chuàng)建一個(gè)
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注冊(cè)一個(gè)回調(diào),當(dāng)線程銷毀時(shí)赖瞒,順便也銷毀其對(duì)應(yīng)的 RunLoop女揭。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
CFRunLoop相關(guān)類
CFRunLoopRef
RunLoop的CF對(duì)象CFRunLoopModeRef
RunLoop的運(yùn)行模式CFRunLoopSourceRef
各種輸入源 (除計(jì)時(shí)器)CFRunLoopTimerRef
計(jì)時(shí)器源CFRunLoopObserverRef
觀察者
CFRunLoopRef
即RunLoop的CF對(duì)象, 提供各種方法來(lái)管理RunLoop.
CFRunLoopModeRef
即CFRunLoopRef的運(yùn)行模式. 每個(gè)CFRunLoopRef可以有多個(gè)CFRunLoopModeRef, 但在同一時(shí)刻有且僅有一種CFRunLoopModeRef.
幾種常用的mode (或mode組合):
-
kCFRunLoopDefaultMode
默認(rèn)mode -
UITrackingRunLoopMode
追蹤ScrollView滾動(dòng)時(shí)的狀態(tài) -
kCFRunLoopCommonModes
前兩個(gè)mode的組合
ScrollView滾動(dòng)時(shí)NSTimer不工作
主線程中注冊(cè)了kCFRunLoopDefaultMode和UITrackingRunLoopMode這兩個(gè)mode. 當(dāng)有ScrollView發(fā)生滾動(dòng)時(shí), RunLoop從kCFRunLoopDefaultMode切換到UITrackingRunLoopMode, 此模式下計(jì)時(shí)器(NSTimer)將不工作.
CFRunLoopSourceRef
輸入源Source將事件異步傳遞到線程, 事件的來(lái)源取決于輸入來(lái)源的類型. 通常有兩個(gè)類別: 基于端口的輸入源
和自定義輸入源
.
圖中, 左邊是線程循環(huán), 右邊是輸入源. 輸入源1~4分別為基于端口的源, 自定義源, 選擇器源, 以及計(jì)時(shí)器源. 其中, 1和4都屬于基于端口的源, 不同的是所有的計(jì)時(shí)器源都共用一個(gè)端口“Mode Timer Port”,而每個(gè)基于端口的源都有不同的對(duì)應(yīng)端口. 2和3都屬于自定義輸入源, 可以理解為選擇器源是官方為我們定義好的自定義源.
-
Port-Based Sources 基于端口的源
監(jiān)視您的應(yīng)用程序的Mach端口, 由內(nèi)核自動(dòng)發(fā)出信號(hào). 關(guān)于Mach/內(nèi)核/端口, 詳見(jiàn)后文<底層實(shí)現(xiàn)>.
Cocoa和Core Foundation提供了內(nèi)置支持, 用于創(chuàng)建基于端口的輸入源.
例如
- 在Cocoa中栏饮,您根本不需要直接創(chuàng)建輸入源, 而只需創(chuàng)建一個(gè)端口對(duì)象(NSPort), 然后使用端口對(duì)象的方法將該端口添加到運(yùn)行循環(huán)中吧兔。端口對(duì)象為您處理所需輸入源的創(chuàng)建和配置。
- 在Core Foundation中袍嬉,您必須手動(dòng)創(chuàng)建端口及其運(yùn)行循環(huán)源境蔼。在這兩種情況下灶平,都使用與端口不透明類型(CFMachPortRef、CFMessagePortRef或CFSocketRef)關(guān)聯(lián)的函數(shù)來(lái)創(chuàng)建適當(dāng)?shù)膶?duì)象箍土。
Custom Input Sources 自定義輸入源
監(jiān)視事件的定制源, 從另一個(gè)線程手動(dòng)發(fā)出信號(hào).
參見(jiàn)定義自定義輸入源Cocoa Perform Selector Sources 選擇器源 (屬于自定義輸入源)
該源可讓您在任何線程上執(zhí)行選擇器. 與基于端口的源不同, 執(zhí)行選擇器源在執(zhí)行選擇器后將其自身從運(yùn)行循環(huán)中刪除.
注意: 在另一個(gè)線程上執(zhí)行選擇器時(shí), 目標(biāo)線程必須開啟了RunLoop.
CFRunLoopTimerRef
計(jì)時(shí)器源, 屬于端口輸入源, 在將來(lái)的預(yù)設(shè)時(shí)間將事件同步傳遞到您的線程.
如果timer不支持當(dāng)前RunLoop的模式, 則不會(huì)觸發(fā); 直到RunLoop切換到timer所支持的模式, 才會(huì)觸發(fā); 如果RunLoop沒(méi)有運(yùn)行, 則timer永遠(yuǎn)不會(huì)觸發(fā).
重復(fù)計(jì)時(shí)器會(huì)根據(jù)計(jì)劃的觸發(fā)時(shí)間(而不是實(shí)際的觸發(fā)時(shí)間)自動(dòng)重新計(jì)劃自身. 例如逢享,如果 timer 每5秒觸發(fā)一次, 則即使實(shí)際觸發(fā)時(shí)間被延遲, 計(jì)劃的觸發(fā)時(shí)間也將始終落在原始的5秒時(shí)間間隔上.如果觸發(fā)時(shí)間延遲得太久, 以致錯(cuò)過(guò)了一個(gè)或多個(gè)計(jì)劃的觸發(fā)時(shí)間, 則 timer 將在錯(cuò)過(guò)的時(shí)間段內(nèi)僅觸發(fā)一次. 在錯(cuò)過(guò)了一段時(shí)間后觸發(fā)后, 計(jì)時(shí)器將重新安排為下一個(gè)計(jì)劃的觸發(fā)時(shí)間.
CFRunLoopObserverRef
觀察者 (監(jiān)聽(tīng)器), 隨時(shí)通知外部當(dāng)前RunLoop的運(yùn)行狀態(tài).
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進(jìn)入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
};
Call out
在開發(fā)過(guò)程中幾乎所有的操作都是通過(guò)Call out進(jìn)行回調(diào)的(無(wú)論是Observer的狀態(tài)通知還是Timer、Source的處理)吴藻,而系統(tǒng)在回調(diào)時(shí)通常使用如下幾個(gè)函數(shù)進(jìn)行回調(diào)(換句話說(shuō)你的代碼其實(shí)最終都是通過(guò)下面幾個(gè)函數(shù)來(lái)負(fù)責(zé)調(diào)用的瞒爬,即使你自己監(jiān)聽(tīng)Observer也會(huì)先調(diào)用下面的函數(shù)然后間接通知你,所以在調(diào)用堆棧中經(jīng)彻当ぃ看到這些函數(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__();
例如在控制器的touchBegin中打入斷點(diǎn)查看堆棧(由于UIEvent是Source0侧但,所以可以看到一個(gè)Source0的Call out函數(shù)CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION調(diào)用)(圖片來(lái)源):
運(yùn)行流程
線程的運(yùn)行循環(huán)都會(huì)處理事件源并為所有已注冊(cè)的觀察者生成通知. RunLoop 內(nèi)部的邏輯大致如下 (圖片來(lái)源):
Source0: 自定義輸入源
Source1: 基于端口的輸入源
可以看到, 實(shí)際上 RunLoop 內(nèi)部是一個(gè) do-while 循環(huán). 當(dāng)你調(diào)用 CFRunLoopRun() 時(shí), 線程就會(huì)一直停留在這個(gè)循環(huán)里; 直到超時(shí)或被手動(dòng)停止, 該循環(huán)才會(huì)退出.
底層實(shí)現(xiàn)
RunLoop 的核心是基于 mach port 的,其進(jìn)入休眠時(shí)調(diào)用的函數(shù)是 mach_msg()航罗。為了解釋這個(gè)邏輯禀横,下面稍微介紹一下 OSX/iOS 的系統(tǒng)架構(gòu)。圖片來(lái)源
Darwin 即操作系統(tǒng)的核心, 包括系統(tǒng)內(nèi)核粥血、驅(qū)動(dòng)燕侠、Shell 等內(nèi)容. 我們?cè)偕钊肟匆幌?Darwin 這個(gè)核心的架構(gòu) (圖片來(lái)源):
其中,在硬件層上面的三個(gè)組成部分:Mach立莉、BSD、IOKit (還包括一些上面沒(méi)標(biāo)注的內(nèi)容)七问,共同組成了 XNU 內(nèi)核蜓耻。
- XNU 內(nèi)核的內(nèi)環(huán)被稱作 Mach,其作為一個(gè)微內(nèi)核,僅提供了諸如處理器調(diào)度、IPC (進(jìn)程間通信)等非常少量的基礎(chǔ)服務(wù)晒衩。
- BSD 層可以看作圍繞 Mach 層的一個(gè)外環(huán)柿隙,其提供了諸如進(jìn)程管理、文件系統(tǒng)和網(wǎng)絡(luò)等功能誓竿。
- IOKit 層是為設(shè)備驅(qū)動(dòng)提供了一個(gè)面向?qū)ο?C++)的一個(gè)框架。
在Mach中,進(jìn)程蔼卡、線程間的通信是以消息的方式來(lái)完成的,消息在兩個(gè)Port之間進(jìn)行傳遞(這也正是Source1之所以稱之為Port-based Source的原因挣磨,因?yàn)樗褪且揽肯到y(tǒng)發(fā)送消息到指定的Port來(lái)觸發(fā)的)雇逞。消息的發(fā)送和接收使用<mach/message.h>中的mach_msg()函數(shù).
什么時(shí)候使用
我們只需在子線程創(chuàng)建的時(shí)候決定是否需要運(yùn)行RunLoop (主線程中自動(dòng)創(chuàng)建并運(yùn)行). 當(dāng)然, 我們無(wú)需在所有情況下都啟動(dòng)線程的運(yùn)行循環(huán). 例如, 如果使用線程執(zhí)行一些很耗時(shí)的任務(wù), 則可以避免啟動(dòng)RunLoop.
RunLoop用于需要與線程更多交互的情況. 例如:
- 使用端口或自定義輸入源與其他線程進(jìn)行通信
- 在線程上使用計(jì)時(shí)器
- 使用任何
performSelector
方法 - 保持線程執(zhí)行定期任務(wù)
應(yīng)用
NSTimer
NSTimer 是基于 RunLoop 運(yùn)行的 (對(duì)應(yīng)于CFRunloopTimerRef), 所以使用 NSTimer 之前必須注冊(cè)到 RunLoop, 但是 RunLoop 為了節(jié)省資源并不會(huì)在非常準(zhǔn)確的時(shí)間點(diǎn)調(diào)用定時(shí)器.
NSTimer 的創(chuàng)建通常有兩種方式: 一種是timerWithXXX
, 另一種scheduedTimerWithXXX
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
二者最大的區(qū)別是, scheduedTimerWithXXX
除了創(chuàng)建一個(gè)定時(shí)器外會(huì)自動(dòng)以NSDefaultRunLoopMode
添加到當(dāng)前線程 RunLoop 中, 而不添加到 RunLoop 中的 NSTimer 是無(wú)法正常工作的.
AutoreleasePool
AutoreleasePool與RunLoop并沒(méi)有直接的關(guān)系, 之所以將兩個(gè)話題放到一起討論最主要的原因是因?yàn)樵趇OS應(yīng)用啟動(dòng)后會(huì)注冊(cè)兩個(gè)Observer管理和維護(hù)AutoreleasePool. 這兩個(gè)Observer是和自動(dòng)釋放池相關(guān)的兩個(gè)監(jiān)聽(tīng).
不妨在應(yīng)用程序剛剛啟動(dòng)時(shí)打印currentRunLoop可以看到系統(tǒng)默認(rèn)注冊(cè)了很多個(gè)Observer,其中有兩個(gè)Observer的callout如下:
<CFRunLoopObserver 0x6080001246a0 [0x101f81df0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
<CFRunLoopObserver 0x608000124420 [0x101f81df0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
第一個(gè) Observer 監(jiān)視的事件是 Entry (即將進(jìn)入Loop), 它會(huì)回調(diào)objc_autoreleasePoolPush()創(chuàng)建自動(dòng)釋放池, 并向當(dāng)前的AutoreleasePoolPage增加一個(gè)哨兵對(duì)象標(biāo)志. 這個(gè)Observer的order是-2147483647, 優(yōu)先級(jí)最高, 保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前.
第二個(gè) Observer 監(jiān)視了兩個(gè)事件: BeforeWaiting (準(zhǔn)備進(jìn)入休眠) 和 Exit (即將退出Loop) . BeforeWaiting時(shí)調(diào)用方法釋放舊的池并創(chuàng)建新池茁裙;Exit時(shí)調(diào)用方法來(lái)釋放自動(dòng)釋放池. 這個(gè)Observer的order是2147483647, 優(yōu)先級(jí)最低, 保證其釋放池子發(fā)生在其他所有回調(diào)之后塘砸。
在主線程執(zhí)行的代碼,通常是寫在諸如事件回調(diào)晤锥、Timer回調(diào)內(nèi)的掉蔬。這些回調(diào)會(huì)被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著廊宪,所以不會(huì)出現(xiàn)內(nèi)存泄漏,開發(fā)者也不必顯示創(chuàng)建 Pool 了女轿。
UI更新
當(dāng)在操作 UI 時(shí), 比如改變了 Frame箭启、更新了 UIView/CALayer 的層次時(shí), 或者手動(dòng)調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后, 這個(gè) UIView/CALayer 就被標(biāo)記為待處理, 并被提交到一個(gè)全局的容器去, 等待下一次RunLoop運(yùn)行時(shí)更新UI. 但是如果當(dāng)前正在執(zhí)行大量的邏輯運(yùn)算可能UI的更新就會(huì)比較卡.
蘋果注冊(cè)了一個(gè) Observer 監(jiān)聽(tīng) BeforeWaiting(即將進(jìn)入休眠) 和 Exit (即將退出Loop) 事件,回調(diào)去執(zhí)行一個(gè)很長(zhǎng)的函數(shù):
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
谈喳。這個(gè)函數(shù)里會(huì)遍歷所有待處理的 UIView/CAlayer 以執(zhí)行實(shí)際的繪制和調(diào)整册烈,并更新 UI 界面。
NSURLConnection
NSURLConnection 啟動(dòng)以后就會(huì)不斷調(diào)用delegate方法接收數(shù)據(jù)婿禽,這樣一個(gè)連續(xù)的的動(dòng)作正是基于RunLoop來(lái)運(yùn)行赏僧。
一旦NSURLConnection設(shè)置了delegate會(huì)立即創(chuàng)建一個(gè)線程com.apple.NSURLConnectionLoader,同時(shí)內(nèi)部啟動(dòng)RunLoop并在NSDefaultMode模式下添加4個(gè)Source0扭倾。其中CFHTTPCookieStorage用于處理cookie ;CFMultiplexerSource負(fù)責(zé)各種delegate回調(diào)并在回調(diào)中喚醒delegate內(nèi)部的RunLoop(通常是主線程)來(lái)執(zhí)行實(shí)際操作淀零。
NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的膛壹,但底層仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 線程).