RunLoop 參考:深入理解RunLoop
Runloop 的概念
首先,讓一個線程隨時能處理事件,但是并不退出,這樣的模型通常稱作 Event Loop
,如下:
funcation loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
實現(xiàn)這種模型的關(guān)鍵點在于:如何管理事件/消息,如何讓線程在沒有消息處理時休眠以避免資源浪費,在消息到來時立刻被喚醒。
Runloop
實際上就是一個對象,這個對象管理了其所需要處理的事件和消息,并提供一個入口函數(shù)來執(zhí)行上面的 Event Loop
邏輯。線程執(zhí)行了這個函數(shù)后,就會一直處于這個函數(shù)內(nèi)部 “接收消息->等待->處理”的循環(huán)中,直到這個循環(huán)結(jié)束(如返回 quit
消息),函數(shù)返回。
iOS/MacOS
提供了兩個這樣的對象:NSRunLoop
和 CFRunLoopRef
。
-
CFRunLoopRef
是在CoreFoundation
框架內(nèi)的,它提供了純C
的API
,所有這些API
都是線程安全的。 -
NSRunLoop
是基于CFRunLoopRef
的封裝,提供了面向?qū)ο蟮?API
,但這些API
不是線程安全的。
RunLoop 與線程的關(guān)系
-
RunLoop
是通過p_thread
管理的。蘋果不允許直接創(chuàng)建RunLoop
,它提供了兩個自動獲取的方法:CFRunLoopGetCurrent()
和CFRunLoopGetMain()
。 - 線程和
RunLoop
是一一對應(yīng)的,其關(guān)系保存在一個全局的Dictionary
里。線程剛創(chuàng)建時是沒有RunLoop
的,如果不主動獲取,那它就會一直沒有。RunLoop
的創(chuàng)建是在第一次獲取時,銷毀發(fā)生在線程結(jié)束時。
RunLoop 對外的接口
在 CoreFoundation
里面關(guān)于 RunLoop
有五個類:
- CFRunLoopRef
- CFRunLoopSourceRef
- CFRunLoopObserverRef
- CFRunLoopTimerRef
- CFRunLoopModeRef
其中, CFRunLoopModeRef
類沒有對外暴露,只是通過 CFRunLoopRef
的接口進行了封裝,他們關(guān)系如下:
一個 RunLoop
包含若干 Mode
,每個 Mode
又包含若干 Source/Timer/Observer
。每次調(diào)用 RunLoop
的主函數(shù)時,只能指定其中一個 Mode
,這個 Mode
被稱為 CurrentMode
。如果需要切換 Mode
,只能退出 Loop
,再重新指定一個 Mode 進入。這樣做主要是為了分割不同組的 Source/Timer/Observer
,讓其互不影響。這也是為啥 ScrollView
滑動時,默認 Mode 下計時器停止的原因。
Source/Timer/Observer
被統(tǒng)稱為 mode item
,一個 item
可以同時加入多個 mode
。但一個 item
被重復(fù)加入同一個 mode
是不會有效果的。如果一個 mode
中一個 item
都沒有,則 RunLoop
會直接退出,不進入循環(huán)。
CFRunLoopSourceRef
是事件產(chǎn)生的地方。 Source
有兩個版本: Source0
和 Source1
。
-
Source0
只包含一個回調(diào)(指針),它并不能主動觸發(fā)事件。使用時需要先調(diào)用CFRunLoopSourceSignal(source)
,將這個source
標(biāo)記為待處理, 然后手動調(diào)用CFRunLoopWakeUp(runloop)
來喚醒RunLoop
,讓其處理事件。 -
Source1
包含了一個mach_port
和一個回調(diào)(指針),被用于通過內(nèi)核和其他線程相互發(fā)送消息。這種Source
能主動喚醒RunLoop
的線程。
CFRunLoopTimerRef
CFRunLoopTimerRef
是基于時間的觸發(fā)器,它和 NSTimer
是 toll-free bridged
的,可以混用。其包含一個時間長度和回調(diào)(指針)。當(dāng)其加入到 RunLoop
時,RunLoop
會注冊對應(yīng)的時間點,當(dāng)時間點到時,RunLoop
被喚醒執(zhí)行那個回調(diào)。
CFRunLoopObserverRef
CFRunLoopObserverRef
是觀察者。 每個 Observer
都包含了一個回調(diào)(指針),當(dāng) RunLoop
狀態(tài)發(fā)生變化時,觀察者就能通過回調(diào)接收到這個變化。可以觀測的時間點有以下幾個:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
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 有兩個:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,可以用這兩個 Mode Name 來操作其對應(yīng)的 Mode。
- 可以自定義 Mode。RunLoop 內(nèi)部 Mode 只能增加,不能減少。
RunLoop 內(nèi)部邏輯
RunLoop 內(nèi)部的邏輯大致如下:
蘋果用 RunLoop 實現(xiàn)的功能
APP啟動時,系統(tǒng)默認注冊了5個Mode:
- kCFRunLoopDefaultMode: App的默認 Mode,通常主線程是在這個 Mode 下運行的。
- UITrackingRunLoopMode: 界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響。
- UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用。
- GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到。
- kCFRunLoopCommonModes: 這是一個占位的 Mode,沒有實際作用。
可以在 這里 看到更多的蘋果內(nèi)部的 Mode,但那些 Mode 在開發(fā)中就很難遇到了。
RunLoop 參考:iOS線下分享《RunLoop》by 孫源@sunnyxx
RunLoop
機制
為什么要有 RunLoop
:
- 使程序一直運行并接受用戶輸入
- 決定程序在何時應(yīng)該處理哪些
Event
- 調(diào)用解耦(
Message Quene
) - 節(jié)省
CPU
時間
-
RunLoop
與線程(Thread
)一一綁定并非說是一個Thread
只能對應(yīng)一個RunLoop
, 而是對應(yīng)一個在外層的RunLoop
,RunLoop
可以嵌套使用。 -
1
對n
是通過數(shù)組結(jié)構(gòu)實現(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
定義了兩個Version
的Source
,可以從堆棧信息中查看:-
Source0
: 處理App
內(nèi)部事件、App
自己負責(zé)管理(觸發(fā)),如UIEvent
,CFSocket
等。 -
Source1
: 由RunLoop
內(nèi)核管理,Mach Port
驅(qū)動,如CFMachPort
、CFMessagePort
(都是官方文檔說的。。。) - 如果需要,可以選擇一種來實現(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
};
- 向外部報告當(dāng)前
RunLoop
狀態(tài)的更改。 - 框架中很多機制都由
CFRunLoopObserver
觸發(fā),如CAAnimation
。(其實這個也是猜測,并沒有實質(zhì)文檔說明)
Topic
: CFRunLoopObserver
與 Autorelease Pool
關(guān)系
UIKit
通過RunLoopObserver
在RunLoop
循環(huán)過程中,對Autorelease Pool
進行Pop
和Push
操作,將這次Loop
中產(chǎn)生的Autorelease
對象釋放。視頻中作者測試,是在兩次Sleep
之間。
CFRunLoopMode
-
RunLoop
在同一段時間,只能,并且必須在一種特定的Mode
下Run
。 - 更換
Mode
時,當(dāng)前Loop
會停止,然后重新啟動Loop
。 -
Mode
是iOS App
滑動順暢的關(guān)鍵。 - 可以定制自己的
Mode
。
-
NSDefaultRunLoopMode
: 默認狀態(tài),越是空閑時的狀態(tài)。 -
UITrackingRunLoopMode
: 滑動時的狀態(tài)ScrollView
。 -
UIInitializationRunLoopMode
: 私有(不可見,堆棧追蹤能看到,其他均為猜測) -
NSRunLoopCommonModes
: 包含NSDefaultRunLoopMode
和UITrackingRunLoopMode
兩種狀態(tài)。:
Topic
: UITrackingRunLoopMode
與 Timer
- 這個方法,是將
Timer
加到默認的NSDefaultRunLoopMode
模式下。
[NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:@selector(timeCount)
userInfo:nil
repeats:YES];
- 如果存在
ScrollView
,滑動時,[NSRunLoop currentRunLoop].currentMode
從NSDefaultRunLoopMode
改變?yōu)?UITrackingRunLoopMode
,此時,Timer
暫停,結(jié)束滑動后,Timer
繼續(xù)。如果想要滑動時Timer
正常運行,則將Timer
添加到NSRunLoopCommonModes
模式下即可,即:
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
Topic
: RunLoopMode
的切換
- 滑動前:
NSDefaultRunLoopMode
- 滑動中:
UITrackingRunLoopMode
- 滑動結(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)核將這個線程掛起,停留在mach_msg_trap
狀態(tài)。 - 由另一個線程(或另一個進程中的某個線程)向內(nèi)核發(fā)送這個端口的
msg
后,trap
狀態(tài)被喚醒,Runloop
繼續(xù)開始干活。