引用
一、基本概念
自我理解:RunLoop 就是線程資源管理器。
它的本質(zhì)是一個(gè)事件循環(huán),用于管理線程的生命周期和事件處理。
不斷地從事件源中接收事件,并處理事件(觸摸、定時(shí)器、網(wǎng)絡(luò)等),沒(méi)有事件時(shí)讓線程休眠。
- 線程相關(guān):每個(gè)線程都有一個(gè)對(duì)應(yīng)的 RunLoop,只有主線程的 RunLoop 是自動(dòng)創(chuàng)建并運(yùn)行的,子線程的 RunLoop 需要手動(dòng)啟動(dòng)。
- 模式(Mode):RunLoop 運(yùn)行在不同的模式下,每種模式下可以注冊(cè)不同的事件源(如輸入源、定時(shí)器等)。常見(jiàn)的模式包括 NSDefaultRunLoopMode、UITrackingRunLoopMode 等。
二、RunLoop 的結(jié)構(gòu),組成部分
-
輸入源:用于接收來(lái)自其他線程或應(yīng)用的事件。分為兩類:
- 基于端口的輸入源:系統(tǒng)自動(dòng)觸發(fā)。
- 自定義輸入源:開(kāi)發(fā)者可以自定義的事件(如觸摸事件)。
- 定時(shí)器源:用于處理定時(shí)器事件。NSTimer 就是基于 RunLoop 的定時(shí)器。
-
觀察者Observers:用于監(jiān)聽(tīng) RunLoop 的狀態(tài)變化。例如 RunLoop 即將進(jìn)入休眠、即將處理事件等。
三、 RunLoop 的運(yùn)行流程
RunLoop 的運(yùn)行流程可以簡(jiǎn)化為以下幾個(gè)步驟:
- 通知觀察者:RunLoop 即將啟動(dòng)。
- 處理定時(shí)器:檢查并處理即將觸發(fā)的定時(shí)器。
- 處理輸入源:處理已準(zhǔn)備好的輸入源。
- 進(jìn)入休眠:如果沒(méi)有事件需要處理,RunLoop 進(jìn)入休眠狀態(tài),等待事件喚醒。
- 喚醒:當(dāng)有事件到達(dá)時(shí),RunLoop 被喚醒。
- 處理事件:處理喚醒 RunLoop 的事件。
-
通知觀察者:RunLoop 即將退出。
四、 RunLoop 的模式
RunLoop 可以運(yùn)行在不同的模式下,常見(jiàn)的模式有:
- NSDefaultRunLoopMode:默認(rèn)模式,通常用于處理大多數(shù)事件。
- UITrackingRunLoopMode:用于跟蹤用戶交互事件(如滑動(dòng) UIScrollView)。
-
NSRunLoopCommonModes:一個(gè)集合模式,包含 NSDefaultRunLoopMode 和 UITrackingRunLoopMode。
五、 RunLoop 的常見(jiàn)問(wèn)題和使用場(chǎng)景
每個(gè)線程都有且只有一個(gè) RunLoop,但只有主線程的 RunLoop 是默認(rèn)開(kāi)啟的。
| 線程類型 | RunLoop 狀態(tài) | 說(shuō)明 |
|---|---|---|
| 主線程 | 自動(dòng)開(kāi)啟 | UIApplicationMain() 中自動(dòng)啟動(dòng) |
| 子線程 | 默認(rèn)關(guān)閉 | 需要手動(dòng)獲取并運(yùn)行 |
| 方面 | 說(shuō)明 |
|---|---|
| 定義 | 事件循環(huán),管理線程生命周期 |
| 線程關(guān)系 | 一對(duì)一,主線程自動(dòng)開(kāi)啟 |
| 核心組件 | Mode、Source、Timer、Observer |
| 主要作用 | 保持線程存活、處理事件、節(jié)省 CPU |
| 常見(jiàn)應(yīng)用 | 定時(shí)器、延遲執(zhí)行、卡頓檢測(cè)、保持子線程 |
- 定時(shí)器不準(zhǔn)確:NSTimer 的觸發(fā)依賴于 RunLoop,如果 RunLoop 沒(méi)有運(yùn)行 或 正在處理其他事件,定時(shí)器的觸發(fā)可能會(huì)被延遲。
- RunLoop 阻塞:如果在 RunLoop 中執(zhí)行了耗時(shí)操作,會(huì)導(dǎo)致 RunLoop 無(wú)法及時(shí)處理其他事件,造成界面卡頓。
- 線程?;?常駐:通過(guò)啟動(dòng) RunLoop 可以讓線程常駐,但需要注意在適當(dāng)?shù)臅r(shí)候停止 RunLoop,避免資源浪費(fèi)。
- 定時(shí)器的 Mode 切換問(wèn)題:UIScrollView 滑動(dòng)時(shí),定時(shí)器會(huì)暫停工作。
- 監(jiān)控界面卡頓:檢測(cè)主線程卡頓,用于性能監(jiān)控
- 創(chuàng)建常駐線程:需要長(zhǎng)時(shí)間在后臺(tái)執(zhí)行任務(wù),避免頻繁創(chuàng)建銷毀線程。
引用:CADisplayLink 與 Runloop Observer 都能監(jiān)控卡頓
定時(shí)器不準(zhǔn)確使用:dispatch_source_t、CADisplayLink、NSTimer添加到 CommonModes
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
| 原因 | 影響程度 | 解決方案 |
|---|---|---|
| RunLoop 繁忙 | ????? | 使用 GCD Timer |
| Mode 切換 | ???? | 添加到 CommonModes |
| 線程阻塞 | ???? | 避免阻塞,或用獨(dú)立線程 |
| 系統(tǒng)負(fù)載 | ??? | 優(yōu)化性能 |
| 時(shí)鐘變化 | ?? | 使用 CACurrentMediaTime() |
| 誤差累積 | ?? | 使用 dispatch_source_t |
最佳實(shí)踐:
- 普通定時(shí)任務(wù)(秒級(jí)精度):
NSTimer+ CommonModes - 精確計(jì)時(shí)(毫秒級(jí)):
dispatch_source_t - 動(dòng)畫(huà)幀同步 :
CADisplayLink - 現(xiàn)代開(kāi)發(fā) : 考慮
async/await+Task.sleep
一般的使用功能:
-自動(dòng)釋放池
-延遲回調(diào)
-觸摸事件
-屏幕刷新
一般來(lái)講,一個(gè)線程一次只能執(zhí)行一個(gè)任務(wù),執(zhí)行完成后線程就會(huì)退出(銷毀)。如果我們需要一個(gè)機(jī)制,讓線程能隨時(shí)處理事件但并不退出。
實(shí)現(xiàn)這種模型的關(guān)鍵點(diǎn)在于:如何管理事件/消息,如何讓線程在沒(méi)有處理消息時(shí)休眠,以避免資源占用、在有消息到來(lái)時(shí)立刻被喚醒。
一個(gè)RunLoop就是一個(gè)事件處理循環(huán),用來(lái)不停的調(diào)配工作以及處理輸入事件。使用RunLoop的目的是使你的線程在有工作的時(shí)候工作,沒(méi)有的時(shí)候休眠。NSRunloop可以保持一個(gè)線程一直為活躍狀態(tài),不會(huì)馬上銷毀。
在多線程中使用定時(shí)器必須開(kāi)啟Runloop,因?yàn)橹挥虚_(kāi)啟Runloop保持線程為活躍狀態(tài),定時(shí)器才能運(yùn)行正常。(即:一個(gè)線程對(duì)應(yīng)一個(gè) RunLoop)
# 定時(shí)器
- (void)viewDidLoad
{
[super viewDidLoad];
[self performSelectorInBackground:@selector(multithread) withObject:nil];
}
-(void)multithread {
NSLog(@"HE");
[NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(timeAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
-(void)timeAction {
NSLog(@"Hello");
}
有兩種方法可以讓RunLoop處理事件之前退出:
1、給 runloop設(shè)置超時(shí)時(shí)間
2、通知runloop停止
OSX/iOS 系統(tǒng)中,提供了兩個(gè)這樣的對(duì)象:NSRunLoop 和 CFRunLoopRef。
1、CFRunLoopRef 是在 CoreFoundation 框架內(nèi)的(CF),它提供了純 C 函數(shù)的 API,所有這些 API 都是線程安全的。
2、NSRunLoop 是基于 CFRunLoopRef 的封裝,提供了面向?qū)ο蟮?API,但是這些 API 不是線程安全的。
RunLoop 與 線程 的關(guān)系
首先,iOS 開(kāi)發(fā)中能遇到兩個(gè)線程對(duì)象: pthread_t和 NSThread。過(guò)去蘋(píng)果有份文檔標(biāo)明了 NSThread 只是對(duì) pthread_t 的封裝,但那份文檔已經(jīng)失效了,現(xiàn)在它們也有可能都是直接包裝自最底層的 mach thread。蘋(píng)果并沒(méi)有提供這兩個(gè)對(duì)象相互轉(zhuǎn)換的接口,但不管怎么樣,可以肯定的是 pthread_t 和 NSThread 是一一對(duì)應(yīng)的。比如,你可以通過(guò) pthread_main_thread_np() 或 [NSThread mainThread]來(lái)獲取主線程;也可以通過(guò) pthread_self() 或 [NSThread currentThread] 來(lái)獲取當(dāng)前線程。CFRunLoop 是基于 pthread 來(lái)管理的。
蘋(píng)果不允許直接創(chuàng)建 RunLoop,它只提供了兩個(gè)自動(dòng)獲取的函數(shù):CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 這兩個(gè)函數(shù)內(nèi)部的邏輯大概是下面這樣:
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
static CFSpinLock_t loopsLock; /// 訪問(wèn) loopsDic 時(shí)的鎖(自旋鎖)
/// 獲取一個(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());
}
RunLoop與線程的關(guān)系
GCD 可以同時(shí)處理多個(gè)線程,一個(gè)線程可以處理多個(gè)事件,線程的 使用和睡眠 由RunLoop控制,避免浪費(fèi)系統(tǒng)資源。

一個(gè) RunLoop 包含若干個(gè) Mode,每個(gè) Mode 又包含若干個(gè) Source、Timer、Observer。每次調(diào)用 RunLoop 的主函數(shù)時(shí),只能指定其中一個(gè) Mode,這個(gè)Mode被稱作 CurrentMode。如果需要切換 Mode,只能退出 Loop,再重新指定一個(gè) Mode 進(jìn)入。這樣做主要是為了分隔開(kāi)不同組的 Source/Timer/Observer,讓其互不影響。
(即:每個(gè)事件都要注冊(cè) Observer 觀察者,切換 Mode就是切換事件源)
概念了解
切換事件源:比如兩個(gè)事件 1.一個(gè)文件下載;2.數(shù)據(jù)請(qǐng)求。在同一條線程上,當(dāng)文件下載暫停時(shí),就切換到等待的事件2進(jìn)行處理切換上下文:CPU從一個(gè)進(jìn)程或線程切換到另一個(gè)進(jìn)程或線程。
上下文切換對(duì)系統(tǒng)來(lái)說(shuō)意味著消耗大量的CPU時(shí)間。
RunLoop 對(duì)外的接口
在 CoreFoundation 里面關(guān)于 RunLoop 有5個(gè)類:
- CFRunLoopRef
- CFRunLoopModeRef(
Mode) - CFRunLoopSourceRef(
Source) - CFRunLoopTimerRef(
Timer) - CFRunLoopObserverRef(
Observer)
其中 CFRunLoopModeRef 類并沒(méi)有對(duì)外暴露,只是通過(guò) CFRunLoopRef 的接口進(jìn)行了封裝。他們的關(guān)系如下:

從上面的代碼可以看出,線程和 RunLoop 之間是一一對(duì)應(yīng)的,其關(guān)系是保存在一個(gè)全局的 Dictionary 里。線程剛創(chuàng)建時(shí)并沒(méi)有 RunLoop,如果你不主動(dòng)獲取,那它一直都不會(huì)有。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時(shí),RunLoop 的銷毀是發(fā)生在線程結(jié)束時(shí)。你只能在一個(gè)線程的內(nèi)部獲取其 RunLoop(主線程除外)。
1、基于非端口的源: Source0(待處理源)
??? ? 自定義輸入源
??? ? Cocoa執(zhí)行選擇器源
2、基于端口的源:? ? ? Source1(喚醒源)
CFRunLoopObserverRef,對(duì)應(yīng) observer,表示觀察者。每個(gè) Observer 都包含了一個(gè)回調(diào)(函數(shù)指針),當(dāng) RunLoop 的狀態(tài)發(fā)生變化時(shí),觀察者就能通過(guò)回調(diào)接受到這個(gè)變化??梢杂^測(cè)的時(shí)間點(diǎn)有以下幾個(gè):
- kCFRunLoopEntry,即將進(jìn)入Loop
- kCFRunLoopBeforeTimers,即將處理 Timer
- kCFRunLoopBeforeSources,即將處理 Source
- kCFRunLoopBeforeWaiting,即將進(jìn)入休眠
- kCFRunLoopAfterWaiting,剛從休眠中喚醒
- kCFRunLoopExit,即將退出Loop

1、CFRunLoopSourceRef (Source:事件源)
是事件產(chǎn)生的地方。Source有兩個(gè)版本:Source0 和 Source1。
? Source0:待處理源 只包含了一個(gè)回調(diào)(函數(shù)指針),它并不能主動(dòng)觸發(fā)事件。使用時(shí),你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個(gè) Source 標(biāo)記為待處理,然后手動(dòng)調(diào)用 CFRunLoopWakeUp(runloop) 來(lái)喚醒 RunLoop,讓其處理這個(gè)事件。
? Source1:?jiǎn)拘言?/code> 包含了一個(gè) mach_port 和一個(gè)回調(diào)(函數(shù)指針),被用于通過(guò)內(nèi)核和其他線程相互發(fā)送消息。這種 Source 能主動(dòng)喚醒 RunLoop 的線程,其原理在下面會(huì)講到。
2、CFRunLoopTimerRef (Timer:回調(diào)定時(shí)器)
是基于時(shí)間的觸發(fā)器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一個(gè)時(shí)間長(zhǎng)度和一個(gè)回調(diào)(函數(shù)指針)。當(dāng)其加入到 RunLoop 時(shí),RunLoop會(huì)注冊(cè)對(duì)應(yīng)的時(shí)間點(diǎn),當(dāng)時(shí)間點(diǎn)到時(shí),RunLoop會(huì)被喚醒以執(zhí)行那個(gè)回調(diào)。
3、CFRunLoopObserverRef (Observer:觀察者)
是觀察者,每個(gè) Observer 都包含了一個(gè)回調(diào)(函數(shù)指針),當(dāng) RunLoop 的狀態(tài)發(fā)生變化時(shí),觀察者就能通過(guò)回調(diào)接受到這個(gè)變化。可以觀測(cè)的時(shí)間點(diǎn)有以下幾個(gè):
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
};
上面的 Source/Timer/Observer 被統(tǒng)稱為 mode item,一個(gè) item 可以被同時(shí)加入多個(gè) mode。但一個(gè) item 被重復(fù)加入同一個(gè) mode 時(shí)是不會(huì)有效果的。如果一個(gè) mode 中一個(gè) item 都沒(méi)有,則 RunLoop 會(huì)直接退出,不進(jìn)入循環(huán)。
RunLoop 的 Mode
CFRunLoopMode 和 CFRunLoop 的結(jié)構(gòu)大致如下:
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
...
};
這里有個(gè)概念叫 “CommonModes”:一個(gè) Mode 可以將自己標(biāo)記為”Common”屬性(通過(guò)將其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每當(dāng) RunLoop 的內(nèi)容發(fā)生變化時(shí),RunLoop 都會(huì)自動(dòng)將 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 標(biāo)記的所有Mode里。
應(yīng)用場(chǎng)景舉例:
主線程的 RunLoop 里有兩個(gè)預(yù)置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個(gè) Mode 都已經(jīng)被標(biāo)記為”Common”屬性。DefaultMode 是 App 平時(shí)所處的狀態(tài),TrackingRunLoopMode 是追蹤 ScrollView 滑動(dòng)時(shí)的狀態(tài)。當(dāng)你創(chuàng)建一個(gè) Timer 并加到 DefaultMode 時(shí),Timer 會(huì)得到重復(fù)回調(diào),但此時(shí)滑動(dòng)一個(gè)TableView時(shí),RunLoop 會(huì)將 mode 切換為 TrackingRunLoopMode,這時(shí) Timer 就不會(huì)被回調(diào),并且也不會(huì)影響到滑動(dòng)操作。
即:滑動(dòng) ScrollView/TableView時(shí), DefaultMode(Timer回調(diào)) —> TrackingMode(Timer不回調(diào))
為了解決這個(gè)問(wèn)題:使用NSRunLoopCommonModes模式。
NSRunLoopCommonModes 是runloop中的另一種模式,其作用等價(jià)于NSDefaultRunLoopMode與UITrackingRunLoopMode的結(jié)合,滑動(dòng)scrollview的時(shí)候等價(jià)于UITrackingRunLoopMode,停止滑動(dòng)的時(shí)候等價(jià)于NSDefaultRunLoopMode。
RunLoop 的內(nèi)部邏輯
任何事件觸發(fā),都先獲取runloop,然后注冊(cè)一個(gè) Observer。
根據(jù)蘋(píng)果在文檔里的說(shuō)明,RunLoop 內(nèi)部的邏輯大致如下:

實(shí)際上 RunLoop 就是這樣一個(gè)函數(shù),其內(nèi)部是一個(gè) do-while 循環(huán)。當(dāng)你調(diào)用 CFRunLoopRun() 時(shí),線程就會(huì)一直停留在這個(gè)循環(huán)里;直到超時(shí)或被手動(dòng)停止,該函數(shù)才會(huì)返回。
RunLoop 的底層實(shí)現(xiàn)
從上面代碼可以看到,RunLoop 的核心是基于 mach port 的,其進(jìn)入休眠時(shí)調(diào)用的函數(shù)是 mach_msg()。為了解釋這個(gè)邏輯,下面稍微介紹一下 OSX/iOS 的系統(tǒng)架構(gòu)。

蘋(píng)果官方將整個(gè)系統(tǒng)大致劃分為上述4個(gè)層次:
1、應(yīng)用層:包括用戶能接觸到的圖形應(yīng)用,例如 Spotlight、Aqua、SpringBoard 等。
2、應(yīng)用框架層:即開(kāi)發(fā)人員接觸到的 Cocoa 等框架。
3、核心框架層:包括各種核心框架、OpenGL 等內(nèi)容。
4、Darwin:即操作系統(tǒng)的核心,包括系統(tǒng)內(nèi)核、驅(qū)動(dòng)、Shell 等內(nèi)容,這一層是開(kāi)源的,其所有源碼都可以在 opensource.apple.com 里找到。
Mach
Mach 本身提供的 API 非常有限,而且蘋(píng)果也不鼓勵(lì)使用 Mach 的 API,但是這些API非?;A(chǔ),如果沒(méi)有這些API的話,其他任何工作都無(wú)法實(shí)施。在 Mach 中,所有的東西都是通過(guò)自己的對(duì)象實(shí)現(xiàn)的,進(jìn)程、線程和虛擬內(nèi)存都被稱為“對(duì)象”。和其他架構(gòu)不同, Mach 的對(duì)象間不能直接調(diào)用,只能通過(guò)消息傳遞的方式實(shí)現(xiàn)對(duì)象間的通信。“消息”是 Mach 中最基礎(chǔ)的概念,消息在兩個(gè)端口 (port) 之間傳遞,這就是 Mach 的 IPC (進(jìn)程間通信) 的核心。
一條 Mach 消息實(shí)際上就是一個(gè)二進(jìn)制數(shù)據(jù)包 (BLOB),其頭部定義了當(dāng)前端口local_port 和目標(biāo)端口 remote_port,
發(fā)送和接受消息是通過(guò)同一個(gè) API 進(jìn)行的,其 option 標(biāo)記了消息傳遞的方向:
mach_msg_return_t mach_msg(
mach_msg_header_t *msg,
mach_msg_option_t option,
mach_msg_size_t send_size,
mach_msg_size_t rcv_size,
mach_port_name_t rcv_name,
mach_msg_timeout_t timeout,
mach_port_name_t notify);
為了實(shí)現(xiàn)消息的發(fā)送和接收,mach_msg() 函數(shù)實(shí)際上是調(diào)用了一個(gè) Mach 陷阱 (trap),即函數(shù)mach_msg_trap(),陷阱這個(gè)概念在 Mach 中等同于系統(tǒng)調(diào)用。當(dāng)你在用戶態(tài)調(diào)用 mach_msg_trap() 時(shí)會(huì)觸發(fā)陷阱機(jī)制,切換到內(nèi)核態(tài);內(nèi)核態(tài)中內(nèi)核實(shí)現(xiàn)的 mach_msg() 函數(shù)會(huì)完成實(shí)際的工作,如下圖:

RunLoop 的核心就是一個(gè) mach_msg() (見(jiàn)上面代碼的第7步),RunLoop 調(diào)用這個(gè)函數(shù)去接收消息,如果沒(méi)有別人發(fā)送 port 消息過(guò)來(lái),內(nèi)核會(huì)將線程置于等待狀態(tài)。例如你在模擬器里跑起一個(gè) iOS 的 App,然后在 App 靜止時(shí)點(diǎn)擊暫停,你會(huì)看到主線程調(diào)用棧是停留在 mach_msg_trap() 這個(gè)地方。
系統(tǒng)默認(rèn)注冊(cè)了5個(gè)Mode:
-
kCFRunLoopDefaultMode: App的默認(rèn) Mode,通常主線程是在這個(gè) Mode 下運(yùn)行的。 -
UITrackingRunLoopMode: 界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動(dòng),保證界面滑動(dòng)時(shí)不受其他 Mode 影響。 -
UIInitializationRunLoopMode: 在剛啟動(dòng) App 時(shí)第進(jìn)入的第一個(gè) Mode,啟動(dòng)完成后就不再使用。 -
GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到。 -
kCFRunLoopCommonModes: 這是一個(gè)占位的 Mode,沒(méi)有實(shí)際作用。
AutoreleasePool
App啟動(dòng)后,蘋(píng)果在主線程 RunLoop 里注冊(cè)了兩個(gè) Observer,其回調(diào)都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一個(gè) Observer 監(jiān)視的事件是 Entry(即將進(jìn)入Loop),其回調(diào)內(nèi)會(huì)調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動(dòng)釋放池。其 order 是-2147483647,優(yōu)先級(jí)最高,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。
第二個(gè) Observer 監(jiān)視了兩個(gè)事件: BeforeWaiting(準(zhǔn)備進(jìn)入休眠) 時(shí)調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池;Exit(即將退出Loop) 時(shí)調(diào)用 _objc_autoreleasePoolPop() 來(lái)釋放自動(dòng)釋放池。這個(gè) Observer 的 order 是 2147483647,優(yōu)先級(jí)最低,保證其釋放池子發(fā)生在其他所有回調(diào)之后。
在主線程執(zhí)行的代碼,通常是寫(xiě)在諸如事件回調(diào)、Timer回調(diào)內(nèi)的。這些回調(diào)會(huì)被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著,所以不會(huì)出現(xiàn)內(nèi)存泄漏,開(kāi)發(fā)者也不必顯示創(chuàng)建 Pool 了。
事件響應(yīng)
蘋(píng)果注冊(cè)了一個(gè) Source1 (基于 mach port 的) 用來(lái)接收系統(tǒng)事件,其回調(diào)函數(shù)為 __IOHIDEventSystemClientQueueCallback()。
當(dāng)一個(gè)硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由 IOKit.framework 生成一個(gè) IOHIDEvent 事件并由 SpringBoard 接收。這個(gè)過(guò)程的詳細(xì)情況可以參考這里。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進(jìn)程。隨后蘋(píng)果注冊(cè)的那個(gè) Source1 就會(huì)觸發(fā)回調(diào),并調(diào)用 _UIApplicationHandleEventQueue() 進(jìn)行應(yīng)用內(nèi)部的分發(fā)。
RunLoop是負(fù)責(zé)監(jiān)聽(tīng)事件:觸摸,時(shí)鐘,網(wǎng)絡(luò)等的,在主線程中創(chuàng)建NSURLConnection,RunLoop 可以被啟動(dòng)。
在 「子線程」中 RunLoop是默認(rèn)不啟動(dòng)的,此時(shí)的網(wǎng)絡(luò)事件是不監(jiān)聽(tīng)的??!
所以,我們要啟動(dòng)運(yùn)行循環(huán):[[NSRunLoop currentRunLoop] run];。
注意,一般我們啟動(dòng)RunLoop的時(shí)候不要使用run,使用run來(lái)啟動(dòng)一旦啟動(dòng),就沒(méi)辦法被回收了。這里我們僅僅用來(lái)演示用。
此時(shí),我們算是解決了這個(gè)問(wèn)題。但是這個(gè)子線程是沒(méi)辦法被回收的,所以不能用run,可以需要手動(dòng)的方式來(lái)使runloop啟動(dòng)起來(lái)。當(dāng)然這種方式比較令人不爽。。。
手勢(shì)識(shí)別:注冊(cè)一個(gè) Observer 監(jiān)聽(tīng)
當(dāng)上面的 _UIApplicationHandleEventQueue() 識(shí)別了一個(gè)手勢(shì)時(shí),其首先會(huì)調(diào)用 Cancel 將當(dāng)前的 touchesBegin/Move/End 系列回調(diào)打斷。隨后系統(tǒng)將對(duì)應(yīng)的 UIGestureRecognizer 標(biāo)記為待處理。(即:打斷其他手勢(shì)回調(diào)-> 觸發(fā)手勢(shì) 標(biāo)記為待處理)
蘋(píng)果注冊(cè)了一個(gè) Observer 監(jiān)測(cè) BeforeWaiting (Loop即將進(jìn)入休眠) 事件,這個(gè)Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver(),其內(nèi)部會(huì)獲取所有剛被標(biāo)記為待處理的 GestureRecognizer,并執(zhí)行GestureRecognizer的回調(diào)。
界面更新:注冊(cè)一個(gè) Observer 監(jiān)聽(tīng):即將進(jìn)入休眠、即將退出Loop
當(dāng)在操作 UI 時(shí),比如改變了 Frame、更新了 UIView/CALayer 的層次時(shí),或者手動(dòng)調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個(gè) UIView/CALayer 就被標(biāo)記為待處理,并被提交到一個(gè)全局的容器去。
蘋(píng)果注冊(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 界面。
定時(shí)器:
NSTimer
其實(shí)就是 CFRunLoopTimerRef,他們之間是 toll-free bridged 的。一個(gè) NSTimer 注冊(cè)到 RunLoop 后,RunLoop 會(huì)為其重復(fù)的時(shí)間點(diǎn)注冊(cè)好事件。例如 10:00, 10:10, 10:20 這幾個(gè)時(shí)間點(diǎn)。RunLoop為了節(jié)省資源,并不會(huì)在非常準(zhǔn)確的時(shí)間點(diǎn)回調(diào)這個(gè)Timer。Timer 有個(gè)屬性叫做 Tolerance (寬容度),標(biāo)示了當(dāng)時(shí)間點(diǎn)到后,容許有多少最大誤差。
CADisplayLink
是一個(gè)與屏幕刷新率同步的定時(shí)器,適合用于 UI 動(dòng)畫(huà)或需要與屏幕刷新同步的任務(wù)。(但實(shí)際實(shí)現(xiàn)原理更復(fù)雜,和 NSTimer 并不一樣,其內(nèi)部實(shí)際是操作了一個(gè) Source)。如果在兩次屏幕刷新之間執(zhí)行了一個(gè)長(zhǎng)任務(wù),那其中就會(huì)有一幀被跳過(guò)去(和 NSTimer 相似),造成界面卡頓的感覺(jué)。在快速滑動(dòng)TableView時(shí),即使一幀的卡頓也會(huì)讓用戶有所察覺(jué)。Facebook 開(kāi)源的 AsyncDisplayLink 就是為了解決界面卡頓的問(wèn)題,其內(nèi)部也用到了 RunLoop。
| 定時(shí)器類型 | 精度 | 適用場(chǎng)景 |
|---|---|---|
| CADisplayLink | 高(60 FPS) | UI 動(dòng)畫(huà)、屏幕刷新同步任務(wù) |
| NSTimer | 低 | 一般性定時(shí)任務(wù) |
| DispatchSourceTimer | 高 | 后臺(tái)任務(wù)、高精度定時(shí)任務(wù) |
| NSDate + CFRunLoop | 最高 | 需要極高精度的任務(wù)(如音視頻同步) |
| MTKView / CVDisplayLink | 高 | 圖形渲染任務(wù) |
TableViewCell添加計(jì)時(shí)器
最近項(xiàng)目中需要實(shí)現(xiàn)一個(gè)需求,需要在一個(gè)訂單列表中給訂單添加一個(gè)時(shí)間倒計(jì)時(shí),不是每一個(gè)都顯示,有的話顯示,沒(méi)有的話不顯示。
看到上面的需求,首先想到的是給需要展示的cell添加定時(shí)器,給不需要的cell不添加定時(shí)器,這樣會(huì)出現(xiàn)一些問(wèn)題。
問(wèn)題:
1.1當(dāng)cell復(fù)用,定時(shí)器的開(kāi)啟和銷毀怎么處理?定時(shí)器初值怎么處理?
1.2如果我們?cè)赾ell展示的時(shí)候開(kāi)啟定時(shí)器,那么對(duì)于沒(méi)有展示的cell(定時(shí)器也沒(méi)有開(kāi)啟),怎么保證前臺(tái)倒計(jì)時(shí)和后臺(tái)計(jì)時(shí)同步?
1.3當(dāng)列表中開(kāi)辟多個(gè)計(jì)時(shí)器,性能消耗很嚴(yán)重,這些怎么優(yōu)化?
1.4當(dāng)我們的列表有上拉加載下拉刷新操作的時(shí)候,定時(shí)器的銷毀開(kāi)啟怎么處理?
……
等等這些問(wèn)題,我們?cè)撛趺刺幚恚?br>
其實(shí)上面有些問(wèn)題,如果不存在復(fù)用的情況下是可以解決的,但1.3的問(wèn)題會(huì)很嚴(yán)重,會(huì)出現(xiàn)頁(yè)面卡頓,那么要解決卡頓問(wèn)題,很明顯我們需要減少定時(shí)器創(chuàng)建的數(shù)量,那么減少到多少個(gè)還能完成功能呢?(越少也好,一個(gè)足最好),經(jīng)過(guò)思考決定就只用一個(gè)定時(shí)器來(lái)實(shí)現(xiàn),那么怎么實(shí)現(xiàn)呢,看下面:
//使用viewModel + RAC屬性監(jiān)聽(tīng)
轉(zhuǎn)自:TableViewCell添加計(jì)時(shí)器功能,只需創(chuàng)建一個(gè)定時(shí)器
PerformSelecter
當(dāng)調(diào)用 NSObject 的 performSelecter: afterDelay:后,實(shí)際上其內(nèi)部會(huì)創(chuàng)建一個(gè) Timer 并添加到當(dāng)前線程的 RunLoop 中。所以如果當(dāng)前線程沒(méi)有 RunLoop,則這個(gè)方法會(huì)失效。
當(dāng)調(diào)用 performSelector: onThread:時(shí),實(shí)際上其會(huì)創(chuàng)建一個(gè) Timer 加到對(duì)應(yīng)的線程去,同樣的,如果對(duì)應(yīng)線程沒(méi)有 RunLoop 該方法也會(huì)失效。
關(guān)于GCD
實(shí)際上 RunLoop 底層也會(huì)用到 GCD 的東西,但同時(shí) GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。
當(dāng)調(diào)用 dispatch_async(dispatch_get_main_queue(), block) 時(shí),libDispatch 會(huì)向主線程的 RunLoop 發(fā)送消息,RunLoop會(huì)被喚醒,并從消息中取得這個(gè) block,并在回調(diào) CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE()
cfrunloop_is_servicing_the_main_dispatch_queue()
里執(zhí)行這個(gè) block。但這個(gè)邏輯僅限于 dispatch 到主線程,dispatch 到其他線程仍然是由 libDispatch 處理的。
關(guān)于網(wǎng)絡(luò)請(qǐng)求
iOS 中,關(guān)于網(wǎng)絡(luò)請(qǐng)求的接口自下至上有如下幾層:
CFSocket
CFNetwork -> ASIHttpRequest
NSURLConnection -> AFNetworking
NSURLSession -> AFNetworking2, Alamofire
CFSocket 是最底層的接口,只負(fù)責(zé) socket 通信。
CFNetwork 是基于 CFSocket 等接口的上層封裝,ASIHttpRequest 工作于這一層。
NSURLConnection 是基于 CFNetwork 的更高層的封裝,提供面向?qū)ο蟮慕涌?,AFNetworking 工作于這一層。(NSURLConnection會(huì)阻塞主線程,在iOS8后被棄用)
引用:iOS進(jìn)階_NSURLConnection被棄用的原因,Connection的缺點(diǎn)
1、沒(méi)有下載進(jìn)度,會(huì)影響用戶體驗(yàn)(計(jì)算總值和下載數(shù)據(jù))
2、內(nèi)存偏高,有一個(gè)最大的峰值
? ? 1.保存完成一次性寫(xiě)入磁盤(pán)
? ? 2.邊下載邊寫(xiě)入
3、會(huì)阻塞主線程
-
NSURLSession是 iOS7 中新增的接口,表面上是和NSURLConnection并列的,但底層仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 線程),AFNetworking2 和 Alamofire 工作于這一層。
AsyncDisplayKit:(ASDK:異步渲染框架)
AsyncDisplayKit 是 Facebook 推出的用于保持界面流暢性的框架,其原理大致如下:
UI 線程中一旦出現(xiàn)繁重的任務(wù)就會(huì)導(dǎo)致界面卡頓,這類任務(wù)通常分為3類:排版、繪制、UI對(duì)象操作。
其中前兩類操作可以通過(guò)各種方法扔到后臺(tái)線程執(zhí)行,而最后一類操作只能在主線程完成,并且有時(shí)后面的操作需要依賴前面操作的結(jié)果 (例如TextView創(chuàng)建時(shí)可能需要提前計(jì)算出文本的大?。?。ASDK 所做的,就是盡量將能放入后臺(tái)的任務(wù)放入后臺(tái),不能的則盡量推遲 (例如視圖的創(chuàng)建、屬性的調(diào)整)。
為此,ASDK 創(chuàng)建了一個(gè)名為 ASDisplayNode 的對(duì)象。例如 frame、backgroundColor等,所有這些屬性都可以在后臺(tái)線程更改。
ASDK 仿照 QuartzCore/UIKit 框架的模式,實(shí)現(xiàn)了一套類似的界面更新的機(jī)制:即在主線程的 RunLoop 中添加一個(gè) Observer,監(jiān)聽(tīng)以下兩個(gè)事件:
kCFRunLoopBeforeWaiting:(即將進(jìn)入休眠)-
kCFRunLoopExit:(即將退出Loop)
在收到回調(diào)時(shí),遍歷所有之前放入隊(duì)列的待處理的任務(wù),然后一一執(zhí)行。
引用1:NSRunLoopCommonModes和NSDefaultRunLoopMode區(qū)別(Timer)
NSRunLoopCommonModes,這個(gè)模式等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的結(jié)合。
引用2:iOS深入理解定時(shí)器
NSTimer和CADisplayLink只會(huì)在創(chuàng)建timer的線程接收到時(shí)鐘回調(diào),線程在沒(méi)有任務(wù)的時(shí)候 10s 左右會(huì)被操作系統(tǒng)回收,注冊(cè)了NSTimer和CADisplayLink定時(shí)任務(wù)的線程,直到定時(shí)器銷毀后10秒左右才會(huì)被回收。
引用:
CFRunLoopRef 的代碼是開(kāi)源的,你可以在這里 CoreFoundation 下載到整個(gè)源碼來(lái)查看。
(Update: Swift 開(kāi)源后,蘋(píng)果又維護(hù)了一個(gè)跨平臺(tái)的 CoreFoundation 版本,這個(gè)版本的源碼可能和現(xiàn)有 iOS 系統(tǒng)中的實(shí)現(xiàn)略不一樣,但更容易編譯,而且已經(jīng)適配了 Linux/Windows。)
參考:
深入理解RunLoop
runloop 的 mode 作用是什么?
iOS事件傳遞與響應(yīng)鏈
1、iOS事件響應(yīng)鏈&傳遞鏈
2、 史上最詳細(xì)的iOS事件的傳遞和響機(jī)制-原理篇