RunLoop 參考:深入理解RunLoop
Runloop 的概念
首先,讓一個(gè)線程隨時(shí)能處理事件,但是并不退出,這樣的模型通常稱作 Event Loop,如下:
funcation loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
實(shí)現(xiàn)這種模型的關(guān)鍵點(diǎn)在于:如何管理事件/消息,如何讓線程在沒有消息處理時(shí)休眠以避免資源浪費(fèi),在消息到來時(shí)立刻被喚醒。
Runloop 實(shí)際上就是一個(gè)對(duì)象,這個(gè)對(duì)象管理了其所需要處理的事件和消息,并提供一個(gè)入口函數(shù)來執(zhí)行上面的 Event Loop 邏輯。線程執(zhí)行了這個(gè)函數(shù)后,就會(huì)一直處于這個(gè)函數(shù)內(nèi)部 “接收消息->等待->處理”的循環(huán)中,直到這個(gè)循環(huán)結(jié)束(如返回 quit 消息),函數(shù)返回。
iOS/MacOS 提供了兩個(gè)這樣的對(duì)象:NSRunLoop 和 CFRunLoopRef。
-
CFRunLoopRef是在CoreFoundation框架內(nèi)的,它提供了純C的API,所有這些API都是線程安全的。 -
NSRunLoop是基于CFRunLoopRef的封裝,提供了面向?qū)ο蟮?API,但這些API不是線程安全的。
RunLoop 與線程的關(guān)系
-
RunLoop是通過p_thread管理的。蘋果不允許直接創(chuàng)建RunLoop,它提供了兩個(gè)自動(dòng)獲取的方法:CFRunLoopGetCurrent()和CFRunLoopGetMain()。 - 線程和
RunLoop是一一對(duì)應(yīng)的,其關(guān)系保存在一個(gè)全局的Dictionary里。線程剛創(chuàng)建時(shí)是沒有RunLoop的,如果不主動(dòng)獲取,那它就會(huì)一直沒有。RunLoop的創(chuàng)建是在第一次獲取時(shí),銷毀發(fā)生在線程結(jié)束時(shí)。
RunLoop 對(duì)外的接口
在 CoreFoundation 里面關(guān)于 RunLoop 有五個(gè)類:
- CFRunLoopRef
- CFRunLoopSourceRef
- CFRunLoopObserverRef
- CFRunLoopTimerRef
- CFRunLoopModeRef
其中, CFRunLoopModeRef 類沒有對(duì)外暴露,只是通過 CFRunLoopRef 的接口進(jìn)行了封裝,他們關(guān)系如下:

一個(gè) RunLoop 包含若干 Mode,每個(gè) Mode 又包含若干 Source/Timer/Observer。每次調(diào)用 RunLoop 的主函數(shù)時(shí),只能指定其中一個(gè) Mode,這個(gè) Mode 被稱為 CurrentMode。如果需要切換 Mode,只能退出 Loop,再重新指定一個(gè) Mode 進(jìn)入。這樣做主要是為了分割不同組的 Source/Timer/Observer,讓其互不影響。這也是為啥 ScrollView 滑動(dòng)時(shí),默認(rèn) Mode 下計(jì)時(shí)器停止的原因。
Source/Timer/Observer 被統(tǒng)稱為 mode item,一個(gè) item 可以同時(shí)加入多個(gè) mode。但一個(gè) item 被重復(fù)加入同一個(gè) mode 是不會(huì)有效果的。如果一個(gè) mode 中一個(gè) item 都沒有,則 RunLoop 會(huì)直接退出,不進(jìn)入循環(huán)。
CFRunLoopSourceRef
是事件產(chǎn)生的地方。 Source 有兩個(gè)版本: Source0 和 Source1。
-
Source0只包含一個(gè)回調(diào)(指針),它并不能主動(dòng)觸發(fā)事件。使用時(shí)需要先調(diào)用CFRunLoopSourceSignal(source),將這個(gè)source標(biāo)記為待處理, 然后手動(dòng)調(diào)用CFRunLoopWakeUp(runloop)來喚醒RunLoop,讓其處理事件。 -
Source1包含了一個(gè)mach_port和一個(gè)回調(diào)(指針),被用于通過內(nèi)核和其他線程相互發(fā)送消息。這種Source能主動(dòng)喚醒RunLoop的線程。
CFRunLoopTimerRef
CFRunLoopTimerRef 是基于時(shí)間的觸發(fā)器,它和 NSTimer 是 toll-free bridged 的,可以混用。其包含一個(gè)時(shí)間長度和回調(diào)(指針)。當(dāng)其加入到 RunLoop 時(shí),RunLoop 會(huì)注冊對(duì)應(yīng)的時(shí)間點(diǎn),當(dāng)時(shí)間點(diǎn)到時(shí),RunLoop 被喚醒執(zhí)行那個(gè)回調(diào)。
CFRunLoopObserverRef
CFRunLoopObserverRef 是觀察者。 每個(gè) Observer 都包含了一個(gè)回調(diào)(指針),當(dāng) RunLoop 狀態(tài)發(fā)生變化時(shí),觀察者就能通過回調(diào)接收到這個(gè)變化??梢杂^測的時(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
};
RunLoop 的 Mode
CFRunLoopMode 和 CFRunLoop` 的結(jié)構(gòu)如下:
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};
篩選出比較關(guān)鍵的信息,大致如下:
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 有兩個(gè):kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,可以用這兩個(gè) Mode Name 來操作其對(duì)應(yīng)的 Mode。
- 可以自定義 Mode。RunLoop 內(nèi)部 Mode 只能增加,不能減少。
RunLoop 內(nèi)部邏輯
RunLoop 內(nèi)部的邏輯大致如下:

蘋果用 RunLoop 實(shí)現(xiàn)的功能
APP啟動(dòng)時(shí),系統(tǒng)默認(rèn)注冊了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,沒有實(shí)際作用。
可以在 這里 看到更多的蘋果內(nèi)部的 Mode,但那些 Mode 在開發(fā)中就很難遇到了。
RunLoop 參考:iOS線下分享《RunLoop》by 孫源@sunnyxx
RunLoop 機(jī)制
為什么要有 RunLoop:
- 使程序一直運(yùn)行并接受用戶輸入
- 決定程序在何時(shí)應(yīng)該處理哪些
Event - 調(diào)用解耦(
Message Quene) - 節(jié)省
CPU時(shí)間

-
RunLoop與線程(Thread)一一綁定并非說是一個(gè)Thread只能對(duì)應(yīng)一個(gè)RunLoop, 而是對(duì)應(yīng)一個(gè)在外層的RunLoop,RunLoop可以嵌套使用。 -
1對(duì)n是通過數(shù)組結(jié)構(gòu)實(shí)現(xiàn)的。
CFRunLoopTimer
我們常用的 Timer 相關(guān)的,都是基于 CFRunLoopTimer 的封裝,如:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti
invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti
invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
或者延遲執(zhí)行:
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument
afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
或者屏幕刷新頻率 CADisplayLink:
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
CFRunLoopSource
-
Source是RunLoop的數(shù)據(jù)抽象類(Protocol) -
RunLoop定義了兩個(gè)Version的Source,可以從堆棧信息中查看:-
Source0: 處理App內(nèi)部事件、App自己負(fù)責(zé)管理(觸發(fā)),如UIEvent,CFSocket等。 -
Source1: 由RunLoop內(nèi)核管理,Mach Port驅(qū)動(dòng),如CFMachPort、CFMessagePort(都是官方文檔說的。。。) - 如果需要,可以選擇一種來實(shí)現(xiàn)自己的
Source。(這事兒知道就行了)
-
CFRunLoopObserver
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
- 向外部報(bào)告當(dāng)前
RunLoop狀態(tài)的更改。 - 框架中很多機(jī)制都由
CFRunLoopObserver觸發(fā),如CAAnimation。(其實(shí)這個(gè)也是猜測,并沒有實(shí)質(zhì)文檔說明)
Topic: CFRunLoopObserver 與 Autorelease Pool 關(guān)系
UIKit通過RunLoopObserver在RunLoop循環(huán)過程中,對(duì)Autorelease Pool進(jìn)行Pop和Push操作,將這次Loop中產(chǎn)生的Autorelease對(duì)象釋放。視頻中作者測試,是在兩次Sleep之間。
CFRunLoopMode
-
RunLoop在同一段時(shí)間,只能,并且必須在一種特定的Mode下Run。 - 更換
Mode時(shí),當(dāng)前Loop會(huì)停止,然后重新啟動(dòng)Loop。 -
Mode是iOS App滑動(dòng)順暢的關(guān)鍵。 - 可以定制自己的
Mode。
-
NSDefaultRunLoopMode: 默認(rèn)狀態(tài),越是空閑時(shí)的狀態(tài)。 -
UITrackingRunLoopMode: 滑動(dòng)時(shí)的狀態(tài)ScrollView。 -
UIInitializationRunLoopMode: 私有(不可見,堆棧追蹤能看到,其他均為猜測) -
NSRunLoopCommonModes: 包含NSDefaultRunLoopMode和UITrackingRunLoopMode兩種狀態(tài)。:
Topic: UITrackingRunLoopMode 與 Timer
- 這個(gè)方法,是將
Timer加到默認(rèn)的NSDefaultRunLoopMode模式下。
[NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:@selector(timeCount)
userInfo:nil
repeats:YES];
- 如果存在
ScrollView,滑動(dòng)時(shí),[NSRunLoop currentRunLoop].currentMode從NSDefaultRunLoopMode改變?yōu)?UITrackingRunLoopMode,此時(shí),Timer暫停,結(jié)束滑動(dòng)后,Timer繼續(xù)。如果想要滑動(dòng)時(shí)Timer正常運(yùn)行,則將Timer添加到NSRunLoopCommonModes模式下即可,即:
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
Topic: RunLoopMode 的切換
- 滑動(dòng)前:
NSDefaultRunLoopMode - 滑動(dòng)中:
UITrackingRunLoopMode - 滑動(dòng)結(jié)束后:
NSDefaultRunLoopMode
RunLoop 與 GCD
視頻中這一塊也是在討論,并沒有定論
-
GCD本身與RunLoop沒有關(guān)系。 -
GCD中dispatch到main queue的block被分發(fā)到main runloop中執(zhí)行,dispatch_after同理。
Runloop 的等待與喚醒
- 指定用戶喚醒的
mach_port端口。 - 調(diào)用
mach_msg監(jiān)聽喚醒端口,被喚醒前,系統(tǒng)內(nèi)核將這個(gè)線程掛起,停留在mach_msg_trap狀態(tài)。 - 由另一個(gè)線程(或另一個(gè)進(jìn)程中的某個(gè)線程)向內(nèi)核發(fā)送這個(gè)端口的
msg后,trap狀態(tài)被喚醒,Runloop繼續(xù)開始干活。