前言
這篇文章是兩年前在公司內部分享時候寫的,自己比較滿意文章的成色,就分享出來。
最近在進行社招的過程中,發現很多iOS五年以上的老司機對Runloop機制都不能很好的理解,而Runloop是iOS中非常重要的一個組件,很多性能優化,都需要對Runloop機制有深刻的理解才能進行。所以整理了以前的筆記,抽絲剝繭的分析Runloop的核心機制。
當有持續的異步任務需求時,我們會創建一個獨立的生命周期可控的線程。RunLoop就是控制線程生命周期并接收事件進行處理的機制。是iOS、OSX開發中比較基礎的一個概念。但它也是事件響應與任務處理的核心機制,貫穿整個系統。
RunLoop 基本概念
為什么引入RunLoop機制
一個線程一次只能執行一個任務,執行完成后線程就會退出。RunLoop可以讓線程隨時處理事件但不退出。
RunLoop并不是iOS平臺的專屬概念,在任何平臺的多線程編程中,為控制線程的生命周期,接收處理異步消息都需要類似RunLoop的循環機制實現,Android的Looper就是類似的機制。這種模型還通常被稱作Event Loop。Event Loop在很多系統和框架里都有實現。比如Node.js 的事件處理,Windows程序的消息循環。
實現這種模型的關鍵點在于:
- 如何管理事件和消息
- 如何讓線程在沒有處理消息時休眠,避免無謂的資源占用
- 如何在有消息到來時立即被喚醒。
引入Runloop機制的目的是利用RunLoop機制的特點實現整體省電的效果,并且讓系統和應用可以流暢的運行,提高響應速度,達到極致的用戶體驗。
本質上RunLoop是什么
進程是一家工廠,線程是一個流水線,Run Loop就是流水線上的主管;當工廠接到商家的訂單分配給這個流水線時,Run Loop就啟動這個流水線,讓流水線動起來,生產產品;當產品生產完畢時,Run Loop就會暫時停下流水線,節約資源。
RunLoop管理流水線,流水線才不會因為無所事事被工廠銷毀;而不需要流水線時,就會辭退RunLoop這個主管,即退出線程,把所有資源釋放。
RunLoop實質上是一個對象,這個對象管理了其需要處理的的事件和消息,并提供了入口函數來執行Event Loop 的邏輯。線程執行這個函數后,會一直處于這個函數內部:接受消息->等待->執行 的循環中,直到這個循環結束。
iOS、OSX系統提供了兩個RunLoop系統:
- Foundation: NSRunLoop 是基于CFRunLoopRef的封裝,提供了面向對象的API,這些API不是線程安全的
- Core Foundation: CFRunLoopRef 核心部分,代碼開源,C 語言編寫,跨平臺。所有API都是線程安全的
CFRunLoopRef源代碼: http://opensource.apple.com/tarballs/CF/
RunLoop的特性
- 主線程的RunLoop在應用啟動的時候就會自動創建
- 其他線程則需要在該線程下自己啟動
- 不能直接創建RunLoop
- RunLoop并不是線程安全的,所以需要避免在其他線程上調用當前線程的RunLoop
- RunLoop負責管理autorelease pools
- RunLoop負責處理消息事件,即輸入源事件和計時器事件
RunLoop與線程的關系
iOS開發中有兩個線程對象:pthread_t和NSThread。過去NSThread只是pthread_t的封裝,現在它們都是直接包裝自最底層的mach thread。
pthread_t同NSThread是一一對應的??梢酝ㄟ^ pthread_main_np() 或 [NSThread mainThread]獲取主線程。也可以通過 pthread_self() 或 [NSThread currentThread] 獲取當前線程。CFRunLoop是基于pthread來管理的,NSRunLoop是基于NSThread管理的。
蘋果不允許直接創建RunLoop,但是提供了兩個獲取RunLoop的函數。CFRunLoopGetMain()和CFRunLoopGetCurrent()。
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
/// 獲取一個 pthread 對應的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次進入時,初始化全局Dic,并先為主線程創建一個 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接從 Dictionary 里獲取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到時,創建一個
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注冊一個回調,當線程銷毀時,順便也銷毀其對應的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
線程和RunLoop之間是一一對應的,其關系是保存在一個全局的Dictionary里。線程剛創建時并沒有RunLoop,如果不主動獲取,一直不會有。RunLoop的創建是發生在第一次獲取時
RunLoop的內部組件
一個RunLoop可以包含多個Mode,每個Mode有包含多個Source、Timer、Observer。每次調用RunLoop主入口函數時,只能指定一個Mode,這個Mode被稱作CurrentMode。如果需要切換Mode,需要退出本次循環,再重新指定一個Mode進入。
Source、Timer、Observer 被統稱為 mode item,一個 item 可以被同時加入多個 mode。但一個 item 被重復加入同一個 mode 時是不會有效果的。如果一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出,不進入循環。
Source
事件源,在CF中的類型是 CFRunLoopSourceRef
事件源是事件產生的地方,有兩個類型:Source0 和 Source1。
- Source0 只包含了一個回調函數,這個回調函數是一個函數指針,它不能主動觸發事件。在使用時,需要先調用CFRunLoopSourceSignal(source),將這個Source標記為待處理,然后手動調用,然后調用 CFRunLoopWakeUp(runloop) ,喚醒RunLoop。
- Source1 包含了一個Mach_port 和一個回調函數。主要用于通過內核和其他線程相互發送消息。這種Source能主動喚醒RunLoop的線程。
Timer
計時器,在CF中的類型是 CFRunLoopTimerRef
CF計時器同NSTimer是toll-free bridged 的,可以互相轉換。
包含了一個時間長度和一個回調函數(IMP)。當它加入到RunLoop時,RunLoop會注冊對應的時間點,當時間點到時,RunLoop會被喚醒執行timer中的函數指針。
Observer
觀察者,在CF中的類型是CFRunLoopObserverRef
用于監聽RunLoop的狀態變化
包含了一個回調函數,當RunLoop的狀態發生變化的時候,觀察者可以通過回調接收到這個變化。
有以下狀態:
- kCFRunLoopEntry 即將進入Loop
- KCFRunLoopBeforeTimers 即將處理 Timer
- KCFRunLoopBeforeSources 即將處理 Source
- KCFRunLoopBeforeWaiting 即將進入休眠
- KCFRunLoopAfterWaiting 從休眠中喚醒
- KCFRunLoopExit 退出Loop
Mode
定義
RunLoop Mode就是流水線上支持生產的產品類型,流水線在一個時刻只能在一種模式下運行,生產某一類型的產品。mode item就是訂單。mode主要是為了分隔開不同組的Source、Timer、Observer,讓其互不影響。
結構
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
};
Mode 類型
- 1、Default:NSDefaultRunLoopMode,默認模式,在Run Loop沒有指定Mode的時候,默認就跑在Default Mode下
- 2、Event tracking:UITrackingRunLoopMode,拖動事件
- 3、Connection:NSConnectionReplyMode,用來監聽處理網絡請求NSConnection的事件
- 4、Modal:NSModalPanelRunLoopMode,OS X的Modal面板事件
- 5、 UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用。
- 6、 GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode。
- Common mode:NSRunLoopCommonModes,是一個模式集合,當綁定一個事件源到這個模式集合的時候就相當于綁定到了集合內的每一個模式。
Common Mode
將ModeName添加到RunLoop結構體中的commonModes集合中, 這個mode就具有Common屬性,成為了CommonMode。當RunLoop發生變化或者發生事件的時候,會將commonModeItems中的Source、Timer、Observer同步到具有Common屬性的Mode中。
主線程有兩個預置Mode。kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個Mode都已經具有Common屬性。timer加入到DefaultMode中,默認情況會被調用,但是當ScrollView滾動的時候,RunLoop會將Mode切換為UITrackingRunLoopMode,這是因為為了更好的用戶體驗,在主線程中Event tracking模式的優先級最高。這時timer不會被調用。如果需要這個timer在兩個Mode中都能得到調用,可以將timer分別加入這兩個Mode中。也可以將timer加入到commonModelItems中,這樣會被所有具有Common屬性的Mode調用。
解決方法:
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(timerHandler:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
//或
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[timer fire];
特定時間執行mode
RunLoop可以通過[acceptInputForMode:beforeDate:]和[runMode:beforeDate:]來指定在一段時間內的運行模式。如果不指定的話,RunLoop默認會運行在Default下(不斷重復調用runMode:NSDefaultRunLoopMode beforDate:)
RunLoop 實現邏輯分析
RunLoop核心代碼
// 用DefaultMode啟動
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 0, false);
}
// 用指定的Mode啟動,允許設置RunLoop超時時間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
// RunLoop的實現
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
// 根據modeName找到對應mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
// 如果mode里沒有source、timer或者observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
// 1. 通知 Observers: RunLoop 即將進入 loop。
// Observer會創建AutoReleasePool _objc_autoreleasePoolPush()
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
// 進入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
// 2. 通知 Observers: RunLoop 即將觸發 Timer 回調。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
// 3. 通知 Observers: RunLoop 即將觸發 Source0 (非port) 回調。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
// 執行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
// 4. RunLoop 觸發 Source0 (非port) 回調。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
// 執行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
// 5. 如果有 Source1 (基于port) 處于 ready 狀態,直接處理這個 Source1 然后跳轉去處理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
// 7. 調用 mach_msg 接收 mach_port 的消息。線程將進入休眠, 直到被下面某一個事件喚醒。
// 基于 port 的Source 的事件
// Timer 的執行時間到了
// RunLoop 自身的超時時間到了
// 被其他調用者手動喚醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
// 8. 通知 Observers: RunLoop 的線程剛剛被喚醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
// 收到消息,處理消息。
handle_msg:
// 9.1 如果一個 Timer 到時間了,觸發這個Timer的回調。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
// 9.2 如果有dispatch到main_queue的block,執行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
// 9.3 如果一個 Source1 (基于port) 發出事件了,處理這個事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
// 執行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
// 進入loop時參數說處理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
// 超出傳入參數標記的超時時間了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
// 被外部調用者強制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
// source/timer/observer一個都沒有了
retVal = kCFRunLoopRunFinished;
}
// 如果沒超時,mode里沒空,loop也沒被停止,那繼續loop。
} while (retVal == 0);
}
// 10. 通知 Observers: RunLoop 即將退出。
// observer 釋放AutoReleasePool _objc_autoreleasePoolPop()
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
實際上 RunLoop 其內部是一個 do-while 循環。RunLoop的核心就是一個MachMessage的調用過程,RunLoop調用這個函數去接收消息,如果沒有別人發送port消息過來,RunLoop會進入休眠狀態。內核會將線程置于等待狀態,這個時候whild循環是停止的,相比于一直運行的while循環,會很節省CPU資源,進而達到省電的目的。
因為Source1注冊了MachPort端口,當有Source1事件進來的時候會通過這個port端口喚醒RunLoop繼續執行,當計時器到了時候也會喚醒RunLoop,RunLoop喚醒后會先通過Observer 當前已經處于喚醒狀態,之后會先執行Source1事件,執行完成后再執行timer、Source0。執行完所有的Source0事件后,如果有Source1事件則執行Source1,如果沒有則通知Observe 進入到休眠狀態。完成一個循環。
RunLoop 底層機制分析
操作系統架構
iOS/OSX操作系統核心是Darwin,Darwin包括了硬件層、XNU內核和Darwin庫等組成部分。
XNU內核由IOKit、BSD、Mach三部分組成。
- IOKit 主要負責IO提供硬件通訊功能
- BSD,是一個類Unix系統,負責進程管理、文件系統、網絡等功能。
- Mach是微內核,提供處理器調度、IPC等基礎服務。
Mach通訊
RunLoop的底層實現基于Mach內核通訊實現的。在Mach中,所有的組件都是一個對象。進程、線程、虛擬內存都是對象。Mach對象之間不能直接調用,對象間的通訊是通過消息機制實現的。
Mach 的 IPC 的核心就是消息(message)在兩個端口(port)之間傳遞。
消息實質上是一個二進制數據包,在頭部定義了當前端口和目標端口。
消息的發送接收通過mach_msg()函數實現,mach_msg()函數內部會通過調用mach_msg_trap()函數將現在的用戶態切換到內核態。進入到內核態后,會調用Mach內核的mach_msg()函數完成消息傳遞工作,注意此mach_msg和第一個mach_msg雖然函數名相同,但是實現和意義是完全不同的。
RunLoop的核心機制就是通過調用 mach_msg 等待接受 mach_port 的消息。線程進入休眠, 直到被下面某一個事件喚醒。事件通過mach_port 發送 mach_msg 喚醒RunLoop。
總結
RunLoop是一個do-while 循環,又不是一個do-while 循環。他的工作模式是一個循環,但是他基于mach_port和mach_msg的 休眠\喚醒 機制確保了他可以在無任務的時候休眠,有任務的時候及時喚醒,相比于一個普通循環,不會空轉,不會浪費系統資源。RunLoop又通過不同的工作mode隔離了不同的事件源,使他們的工作互不影響。這才是RunLoop實現省電,流暢,響應速度快,用戶體驗好的根本原因;進而基于RunLoop的組件如計時器、GCD、界面更新、自動釋放池能高效運轉的根本原因。
面試考察點
- 為什么引入Runloop機制,有什么作用或者好處?
引入Runloop機制的目的是利用RunLoop機制的特點實現整體省電的效果,并且讓系統和應用可以流暢的運行,提高響應速度,達到極致的用戶體驗。
- 為什么省電?
主要有兩點:一、因為不做任何操作的時候主線程Runloop會處于退出狀態,不會執行任何空轉邏輯,不執行代碼自然不消耗CPU資源,自然省電。二、Runloop提供一種班車機制,限制如頁面刷新等任務的執行頻率,一次Runloop只執行一次,防止多次重復執行代碼帶來的性能損耗。
- 為什么可以流程運行?
一個app流暢與否的決定性因素是主線程的阻塞率,在iOS系統中runloop每秒執行60次,理論上主線程runloop達到55幀以上的刷新頻率用戶就感覺不到卡頓。
Mode機制,同一時間只執行一個Mode內的Source或者Timer,比如拖動的時候只指定拖動Mode,其他Mode 如Default Mode中的源不會被執行,確保了高速滑動的時候不會有其他邏輯阻礙主線程刷新。
Runloop做的是管理視圖刷新頻率,防止重復運算。由于視圖更新必須在主線程,視圖的重布局和重繪都會占用主線程的執行時間,一次Runloop循環只執行一次可以最大限度的防止重復運算導致的計算浪費。
管理核心動畫。核心動畫有三個樹,其中render tree 是私有的,應用開發無法訪問到。render tree在專用的render server 進程中執行,是真正用來渲染動畫的地方,線程優先級高于主線程。所以即使app主線程阻塞,也不會影響到動畫的繪制工作。既節省了主線程的計算資源,又使動畫可以流暢的執行。
支持異步方法調用,將耗時操作分發到子線程中進行。RunLoop是performSelector的基礎設施。我們使用 performSelector:onThread: 或者 performSelecter:afterDelay: 時,實際上系統會創建一個Timer并添加到當前線程的RunLoop中。
還有其他的點,這里不展開,詳情可閱讀下文應用實踐。
當然Runloop不是萬能的,如果代碼質量差,在一次Runloop循環中執行的時間過久一樣會導致卡頓,所以解決卡頓問題也是程序員能力的體現。
- 如何提高響應速度?
當發生系統事件時,如觸碰事件,系統通過Mach Port 發送 Mach消息主動喚醒Runloop。Mach是搶占式操作系統內核,Mach系統IPC機制就是依靠消息機制實現的,所以效率非常高。
iOS系統中RunLoop的應用實踐在下一篇文章中闡述:深入淺出 RunLoop (2) — 應用實踐