最近看了很多RunLoop的文章,看完很懵逼,決心整理一下,文章中大部分內容都是引用大神們的,但好歹對自己有個交代了,花了一個周天加幾個晚上熬夜完成的,有個產出還是很爽的,不多比比了,下面開始吧。
什么是RunLoop?
RunLoop是一個接收處理異步消息事件的循環,一個循環中:等待事件發生,然后將這個事件送到能處理它的地方。
RunLoop實際上是一個對象,這個對象在循環中用來處理程序運行過程中出現的各種事件(比如說觸摸事件、UI刷新事件、定時器事件、Selector事件)和消息,從而保持程序的持續運行;而且在沒有事件處理的時候,會進入睡眠模式,從而節省CPU資源,提高程序性能。
Event Loop模型偽代碼
int main(int argc, char * argv[]) {
//程序一直運行狀態
while (AppIsRunning) {
//睡眠狀態,等待喚醒事件
id whoWakesMe = SleepForWakingU p();
//得到喚醒事件
id event = GetEvent(whoWakesMe);
//開始處理事件
HandleEvent(event);
}
return 0;
}
- mach kernel屬于蘋果內核,RunLoop依靠它實現了休眠和喚醒而避免了CPU的空轉。
- Runloop是基于pthread進行管理的,pthread是基于c的跨平臺多線程操作底層API。它是mach thread的上層封裝(可以參見Kernel Programming Guide),和NSThread一一對應(而NSThread是一套面向對象的API,所以在iOS開發中我們也幾乎不用直接使用pthread)。
RunLoop的組成
RunLoop構成
CFRunLoop對象可以檢測某個task或者dispatch的輸入事件,當檢測到有輸入源事件,CFRunLoop將會將其加入到線程中進行處理。比方說用戶輸入事件、網絡連接事件、周期性或者延時事件、異步的回調等。
RunLoop可以檢測的事件類型一共有3種,分別是CFRunLoopSource、CFRunLoopTimer、CFRunLoopObserver。可以通過CFRunLoopAddSource, CFRunLoopAddTimer或者CFRunLoopAddObserver添加相應的事件類型。
要讓一個RunLoop跑起來還需要run loop modes,每一個source, timer和observer添加到RunLoop中時必須要與一個模式(CFRunLoopMode)相關聯才可以運行。
上面是對于CFRunLoop官方文檔的解釋
RunLoop的主要組成
RunLoop共包含5個類,但公開的只有Source、Timer、Observer相關的三個類。
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
CFRunLoopSourceRef
source是RunLoop的數據源(輸入源)的抽象類(protocol),Source有兩個版本:Source0 和 Source1
- source0:只包含了一個回調(函數指針),使用時,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然后手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。處理App內部事件,App自己負責管理(出發),如UIEvent(Touch事件等,GS發起到RunLoop運行再到事件回調到UI)、CFSocketRef。
- Source1:由RunLoop和內核管理,由mach_port驅動(特指port-based事件),如CFMachPort、CFMessagePort、NSSocketPort。特別要注意一下Mach port的概念,它是一個輕量級的進程間通訊的方式,可以理解為它是一個通訊通道,假如同時有幾個進程都掛在這個通道上,那么其它進程向這個通道發送消息后,這些掛在這個通道上的進程都可以收到相應的消息。這個Port的概念非常重要,因為它是RunLoop休眠和被喚醒的關鍵,它是RunLoop與系統內核進行消息通訊的窗口。
CFRunLoopTimerRef 是基于時間的觸發器,它和 NSTimer 是toll-free bridged 的,可以混用(底層基于使用mk_timer實現)。它受RunLoop的Mode影響(GCD的定時器不受RunLoop的Mode影響),當其加入到 RunLoop 時,RunLoop會注冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回調。如果線程阻塞或者不在這個Mode下,觸發點將不會執行,一直等到下一個周期時間點觸發。
CFRunLoopObserverRef 是觀察者,每個 Observer 都包含了一個回調(函數指針),當 RunLoop 的狀態發生變化時,觀察者就能通過回調接受到這個變化。可以觀測的時間點有以下幾個
enum CFRunLoopActivity {
kCFRunLoopEntry = (1 << 0), // 即將進入Loop
kCFRunLoopBeforeTimers = (1 << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1 << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1 << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1 << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1 << 7), // 即將退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 包含上面所有狀態
};
typedef enum CFRunLoopActivity CFRunLoopActivity;
這里要提一句的是,timer和source1(也就是基于port的source)可以反復使用,比如timer設置為repeat,port可以持續接收消息,而source0在一次觸發后就會被runloop移除。
上面的 Source/Timer/Observer 被統稱為 mode item,一個 item 可以被同時加入多個 mode。但一個 item 被重復加入同一個 mode 時是不會有效果的。如果一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出,不進入循環。
RunLoop主要處理以下6類事件
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__();
RunLoop的Mode
CFRunLoopMode 和 CFRunLoop的結構大致如下:
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
...
};
一個RunLoop包含了多個Mode,每個Mode又包含了若干個Source/Timer/Observer。每次調用 RunLoop的主函數時,只能指定其中一個Mode,這個Mode被稱作CurrentMode。如果需要切換 Mode,只能退出Loop,再重新指定一個Mode進入。這樣做主要是為了分隔開不同Mode中的Source/Timer/Observer,讓其互不影響。下面是5種Mode
- kCFDefaultRunLoopMode App的默認Mode,通常主線程是在這個Mode下運行
- UITrackingRunLoopMode 界面跟蹤Mode,用于ScrollView追蹤觸摸滑動,保證界面滑動時不受其他Mode影響
- UIInitializationRunLoopMode 在剛啟動App時第進入的第一個Mode,啟動完成后就不再使用
- GSEventReceiveRunLoopMode 接受系統事件的內部Mode,通常用不到
- kCFRunLoopCommonModes 這是一個占位用的Mode,不是一種真正的Mode
其中kCFDefaultRunLoopMode、UITrackingRunLoopMode是蘋果公開的,其余的mode都是無法添加的。那為何我們又可以這么用呢
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
什么是CommonModes?
一個 Mode 可以將自己標記為”Common”屬性(通過將其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每當 RunLoop 的內容發生變化時,RunLoop 都會自動將 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 標記的所有Mode里
主線程的 RunLoop 里有 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode,這兩個Mode都已經被標記為”Common”屬性。當你創建一個Timer并加到DefaultMode時,Timer會得到重復回調,但此時滑動一個 scrollView 時,RunLoop 會將 mode 切換為TrackingRunLoopMode,這時Timer就不會被回調,并且也不會影響到滑動操作。
如果想讓scrollView滑動時Timer可以正常調用,一種辦法就是手動將這個 Timer 分別加入這兩個 Mode。另一種方法就是將 Timer 加入到CommonMode 中。
怎么將事件加入到CommonMode?
我們調用上面的代碼將 Timer 加入到CommonMode 時,但實際并沒有 CommonMode,其實系統將這個 Timer 加入到頂層的 RunLoop 的 commonModeItems 中。commonModeItems 會被 RunLoop 自動更新到所有具有”Common”屬性的 Mode 里去。
這一步其實是系統幫我們將Timer加到了kCFRunLoopDefaultMode和UITrackingRunLoopMode中。
在項目中最常用的就是設置NSTimer的Mode,比較簡單這里就不說了。
RunLoop運行機制
當你調用 CFRunLoopRun() 時,線程就會一直停留在這個循環里;直到超時或被手動停止,該函數才會返回。每次線程運行RunLoop都會自動處理之前未處理的消息,并且將消息發送給觀察者,讓事件得到執行。RunLoop運行時首先根據modeName找到對應mode,如果mode里沒有source/timer/observer,直接返回。
/// 用DefaultMode啟動
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, 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。
__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 即將退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
RunLoop的掛起和喚醒
RunLoop的掛起
RunLoop的掛起是通過_CFRunLoopServiceMachPort —call—> mach_msg —call—> mach_msg_trap這個調用順序來告訴內核RunLoop監聽哪個mach_port(上面提到的消息通道),然后等待事件的發生(等待與InputSource、Timer描述內容相關的事件),這樣內核就把RunLoop掛起了,即RunLoop休眠了。
RunLoop的喚醒
這接種情況下會被喚醒
- 存在Source0被標記為待處理,系統調用CFRunLoopWakeUp喚醒線程處理事件
- 定時器時間到了
- RunLoop自身的超時時間到了
- RunLoop外部調用者喚醒
當RunLoop被掛起后,如果之前監聽的事件發生了,由另一個線程(或另一個進程中的某個線程)向內核發送這個mach_port的msg后,trap狀態被喚醒,RunLoop繼續運行
處理事件
- 如果一個 Timer 到時間了,觸發這個Timer的回調
- 如果有dispatch到main_queue的block,執行block
- 如果一個 Source1 發出事件了,處理這個事件
事件處理完成進行判斷
- 進入loop時傳入參數指明處理完事件就返回(stopAfterHandle)
- 超出傳入參數標記的超時時間(timeout)
- 被外部調用者強制停止__CFRunLoopIsStopped(runloop)
- source/timer/observer 全都空了__CFRunLoopModeIsEmpty(runloop, currentMode)
RunLoop 的底層實現
關于這個大家可以看ibireme的深入理解RunLoop一文,我這里選擇一些覺得比較重要又不是那么難懂的。
Mach消息發送機制看這篇文章Mach消息發送機制
為了實現消息的發送和接收,mach_msg() 函數實際上是調用了一個 Mach 陷阱 (trap),即函數mach_msg_trap(),陷阱這個概念在 Mach 中等同于系統調用。當你在用戶態調用 mach_msg_trap() 時會觸發陷阱機制,切換到內核態;內核態中內核實現的 mach_msg() 函數會完成實際的工作,如下圖:
RunLoop 的核心就是一個 mach_msg() (見上面代碼的第7步),RunLoop 調用這個函數去接收消息,如果沒有別人發送 port 消息過來,內核會將線程置于等待狀態。例如你在模擬器里跑起一個 iOS 的 App,然后在 App 靜止時點擊暫停,你會看到主線程調用棧是停留在 mach_msg_trap() 這個地方。
RunLoop和線程
RunLoop和線程是息息相關的,我們知道線程的作用是用來執行特定的一個或多個任務,但是在默認情況下,線程執行完之后就會退出,就不能再執行任務了。這時我們就需要采用一種方式來讓線程能夠處理任務,并不退出。所以,我們就有了RunLoop。
iOS開發中能遇到兩個線程對象: pthread_t和NSThread,pthread_t和NSThread 是一一對應的。比如,你可以通過 pthread_main_thread_np()或 [NSThread mainThread]來獲取主線程;也可以通過pthread_self()或[NSThread currentThread]來獲取當前線程。CFRunLoop 是基于 pthread 來管理的。
線程與RunLoop是一一對應的關系(對應關系保存在一個全局的Dictionary里),線程創建之后是沒有RunLoop的(主線程除外),RunLoop的創建是發生在第一次獲取時,銷毀則是在線程結束的時候。只能在當前線程中操作當前線程的RunLoop,而不能去操作其他線程的RunLoop。
蘋果不允許直接創建RunLoop,但是可以通過[NSRunLoop currentRunLoop]或者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時,則需要手動創建和運行RunLoop(尤其是在子線程中, 主線程中的Main RunLoop除外),我看到別人舉了這么個例子,很有意思
調用[NSTimer scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:]帶有schedule的方法簇來啟動Timer.
此方法會創建Timer并把Timer放到當前線程的RunLoop中,隨后RunLoop會在Timer設定的時間點回調Timer綁定的selector或Invocation。但是,在主線程和子線程中調用此方法的效果是有差異的,即在主線程中調用scheduledTimer方法時timer可以在設定的時間點觸發,但是在子線程里則不能觸發。這是因為子線程中沒有創建RunLoop且更沒有啟動RunLoop,而主線程中的RunLoop默認是創建好的且一直運行著。所以,子線程中需要像下面這樣調用。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] run];
});
那為什么下面這樣調用同樣不會觸發Timer呢?
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSRunLoop currentRunLoop] run];
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer) userInfo:nil repeats:NO];
});
我的分析是:scheduledTimerWithTimeInterval內部在向RunLoop傳遞Timer時是調用與線程實例相關的單例方法[NSRunLoop currentRunLoop]來獲取RunLoop實例的,即RunLoop實例不存在就創建一個與當前線程相關的RunLoop并把Timer傳遞到RunLoop中,存在則直接傳Timer到RunLoop中即可。而在RunLoop開始運行后再向其傳遞Timer時,由于dispatch_async代碼塊里的兩行代碼是順序執行,[[NSRunLoop currentRunLoop] run]是一個沒有結束時間的RunLoop,無法執行到“[NSTimer scheduledTimerWithTimeInterval:…”這一行代碼,Timer也就沒有被加到當前RunLoop中,所以更不會觸發Timer了。
蘋果用 RunLoop 實現的功能
AutoreleasePool
App啟動之后,系統啟動主線程并創建了RunLoop,在main thread中注冊了兩個observer,回調都是_wrapRunLoopWithAutoreleasePoolHandler()
第一個Observer監視的事件
- 即將進入Loop(kCFRunLoopEntry),其回調內會調用 _objc_autoreleasePoolPush() 創建自動釋放池。其order是-2147483647,優先級最高,保證創建釋放池發生在其他所有回調之前。
第二個Observer監視了兩個事件
準備進入休眠(kCFRunLoopBeforeWaiting),此時調用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 來釋放舊的池并創建新的池。
即將退出Loop(kCFRunLoopExit)此時調用 _objc_autoreleasePoolPop()釋放自動釋放池。這個 Observer的order是2147483647,確保池子釋放在所有回調之后。
我們知道AutoRelease對象是被AutoReleasePool管理的,那么AutoRelease對象在什么時候被回收呢?
第一種情況:在我們自己寫的for循環或線程體里,我們都習慣用AutoReleasePool來管理一些臨時變量的autorelease,使得在for循環或線程結束后回收AutoReleasePool的時候來回收AutoRelease臨時變量。
另一種情況:我們在主線程里創建了一些AutoRelease對象,這些對象可不能指望在回收Main AutoReleasePool時才被回收,因為App一直運行的過程中Main AutoReleasePool是不會被回收的。那么這種AutoRelease對象的回收就依賴Main RunLoop的運行狀態,Main RunLoop的Observer會在Main RunLoop結束休眠被喚醒時(kCFRunLoopAfterWaiting狀態)通知UIKit,UIKit收到這一通知后就會調用_CFAutorleasePoolPop方法來回收主線程中的所有AutoRelease對象。
在主線程中執行代碼一般都是寫在事件回調或Timer回調中的,這些回調都被加入了main thread的自動釋放池中,所以在ARC模式下我們不用關心對象什么時候釋放,也不用去創建和管理pool。(如果事件不在主線程中要注意創建自動釋放池,否則可能會出現內存泄漏)。
NSTimer(timer觸發)
上文說到了CFRunLoopTimerRef,其實NSTimer的原型就是CFRunLoopTimerRef。一個Timer注冊 RunLoop 之后,RunLoop 會為這個Timer的重復時間點注冊好事件。有兩點需要注意:
- 但是需要注意的是RunLoop為了節省資源,并不會在非常準確的時間點回調這個Timer。Timer 有個屬性叫做 Tolerance (寬容度),標示了當時間點到后,容許有多少最大誤差。這個誤差默認為0,我們可以手動設置這個誤差。文檔最后還強調了,為了防止時間點偏移,系統有權力給這個屬性設置一個值無論你設置的值是多少,即使RunLoop 模式正確,當前線程并不阻塞,系統依然可能會在 NSTimer 上加上很小的的容差。
- 我們在哪個線程調用 NSTimer 就必須在哪個線程終止
在RunLoop的Mode中也有說到,NSTimer使用的時候注意Mode,比如我之前開發時候用NSTimer寫一個Banner圖片輪播框架,如果不設置Timer的Mode為commonModes那么在滑動TableView的時候Banner就停止輪播
DispatchQueue.global().async {
// 非主線程不能使用 Timer.scheduledTimer進行初始化
// self.timer = Timer.scheduledTimer(timeInterval: 6.0, target: self, selector: #selector(TurnPlayerView.didTurnPlay), userInfo: nil, repeats: false)
if #available(iOS 10.0, *) {
self.timer = Timer(timeInterval: 6.0, repeats: true, block: { (timer) in
self.setContentOffset(CGPoint(x: self.frame.width*2, y: self.contentOffset.y), animated: true)
})
} else {
// Fallback on earlier versions
}
RunLoop.main.add(self.timer!, forMode: RunLoopMode.commonModes)
}
和GCD的關系
- RunLoop底層用到GCD
- RunLoop與GCD并沒有直接關系,但當GCD使用到main_queue時才有關系,如下:
//實驗GCD Timer 與 Runloop的關系,只有當dispatch_get_main_queue時才與RunLoop有關系
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"GCD Timer...");
});
當調用 dispatch_async(dispatch_get_main_queue(), block) 時,libDispatch 會向主線程的 RunLoop 發送消息,RunLoop會被喚醒,并從消息中取得這個 block,并在回調 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里執行這個 block。但這個邏輯僅限于 dispatch 到主線程,dispatch 到其他線程仍然是由 libDispatch 處理的。同理,GCD的dispatch_after在dispatch到main_queue時的timer機制才與RunLoop相關。
PerformSelecter
NSObject的performSelecter:afterDelay: 實際上其內部會創建一個 Timer 并添加到當前線程的 RunLoop 中。所以如果當前線程沒有 RunLoop,則這個方法會失效。
NSObject的performSelector:onThread: 實際上其會創建一個 Timer 加到對應的線程去,同樣的,如果對應線程沒有 RunLoop 該方法也會失效。
其實這種方式有種說法也叫創建常駐線程(內存),AFNetworking也用到這種技法。舉個例子,如果把RunLoop去掉,那么test方法就不會執行。
class SecondViewController: UIViewController {
var thread: Thread!
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.red
thread = Thread.init(target: self, selector: #selector(SecondViewController.run), object: nil)
thread.start()
}
@objc func run() {
print("run -- ")
RunLoop.current.add(Port(), forMode: .defaultRunLoopMode)
RunLoop.current.run()
}
@objc func test() {
print("test -- \(Thread.current)")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// self.test()
self.perform(#selector(SecondViewController.test), on: thread, with: nil, waitUntilDone: false)
}
}
網絡請求
iOS中的網絡請求接口自下而上有這么幾層
其中CFSocket和CFNetwork偏底層,早些時候比較知名的網絡框架AFNetworking是基于NSURLConnection編寫的,iOS7之后新增了NSURLSession,NSURLSession的底層仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 線程),之后AFNetworking和Alamofire就是基于它封裝的了。
通常使用 NSURLConnection 時,會傳入一個 Delegate,當調用了 [connection start] 后,這個 Delegate 就會不停收到事件回調。實際上,start 這個函數的內部會獲取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4個 Source0 (即需要手動觸發的Source)。CFMultiplexerSource 是負責各種 Delegate 回調的,CFHTTPCookieStorage 是處理各種 Cookie 的。
開始網絡傳輸時,NSURLConnection 創建了兩個新線程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。
其中 CFSocket 線程是處理底層 socket 連接的,NSURLConnectionLoader中的RunLoop通過一些基于mach port的Source1接收來自底層CFSocket的通知。當收到通知后,其會在合適的時機向CFMultiplexerSource等Source0發送通知,同時喚醒Delegate線程的RunLoop來讓其處理這些通知。CFMultiplexerSource會在Delegate線程的RunLoop對Delegate執行實際的回調。
事件響應
蘋果注冊了一個 Source1 (基于 mach port 的) 用來接收系統事件,其回調函數為 __IOHIDEventSystemClientQueueCallback()。
當一個硬件事件(觸摸/鎖屏/搖晃等)發生后,首先由 IOKit.framework 生成一個 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨后用 mach port 轉發給需要的App進程。
觸摸事件其實是Source1接收系統事件后在回調 __IOHIDEventSystemClientQueueCallback()內觸發的 Source0,Source0 再觸發的 _UIApplicationHandleEventQueue()。source0一定是要喚醒runloop及時響應并執行的,如果runloop此時在休眠等待系統的 mach_msg事件,那么就會通過source1來喚醒runloop執行。
_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理并包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理屏幕旋轉/發送給 UIWindow 等。
手勢識別
當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調用 Cancel 將當前的 touchesBegin/Move/End 系列回調打斷。隨后系統將對應的 UIGestureRecognizer 標記為待處理。
蘋果注冊了一個 Observer 監測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調函數是 _UIGestureRecognizerUpdateObserver(),其內部會獲取所有剛被標記為待處理的 GestureRecognizer,并執行GestureRecognizer的回調。
當有 UIGestureRecognizer 的變化(創建/銷毀/狀態改變)時,這個回調都會進行相應處理。
UI更新
Core Animation 在 RunLoop 中注冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件 。當在操作 UI 時,比如改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個 UIView/CALayer 就被標記為待處理,并被提交到一個全局的容器去。當Oberver監聽的事件到來時,回調執行函數中會遍歷所有待處理的UIView/CAlayer 以執行實際的繪制和調整,并更新 UI 界面。
如果此處有動畫,通過 DisplayLink 穩定的刷新機制會不斷的喚醒runloop,使得不斷的有機會觸發observer回調,從而根據時間來不斷更新這個動畫的屬性值并繪制出來。
函數內部的調用棧
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];
繪圖和動畫有兩種處理的方式:CPU(中央處理器)和GPU(圖形處理器)
CPU: CPU 中計算顯示內容,比如視圖的創建、布局計算、圖片解碼、文本繪制等
GPU: GPU 進行變換、合成、渲染.
關于CADisplayLink的描述有兩種
CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現原理更復雜,和 NSTimer 并不一樣,其內部實際是操作了一個 Source)。如果在兩次屏幕刷新之間執行了一個長任務,那其中就會有一幀被跳過去(和 NSTimer 相似),造成界面卡頓的感覺。在快速滑動TableView時,即使一幀的卡頓也會讓用戶有所察覺。
CADisplayLink是一個執行頻率(fps)和屏幕刷新相同(可以修改preferredFramesPerSecond改變刷新頻率)的定時器,它也需要加入到RunLoop才能執行。與NSTimer類似,CADisplayLink同樣是基于CFRunloopTimerRef實現,底層使用mk_timer(可以比較加入到RunLoop前后RunLoop中timer的變化)。和NSTimer相比它精度更高(盡管NSTimer也可以修改精度),不過和NStimer類似的是如果遇到大任務它仍然存在丟幀現象。通常情況下CADisaplayLink用于構建幀動畫,看起來相對更加流暢,而NSTimer則有更廣泛的用處。
不管怎么樣CADisplayLink和NSTimer是有很大不同的,詳情可以參考這篇文章CADisplayLink
ibireme根據CADisplayLink的特性寫了個FPS指示器YYFPSLabel,代碼非常少
原理是這樣的:既然CADisplayLink可以以屏幕刷新的頻率調用指定selector,而且iOS系統中正常的屏幕刷新率為60Hz(60次每秒),所以使用 CADisplayLink 的 timestamp 屬性,配合 timer 的執行次數計算得出FPS數
參考文章
深入理解RunLoop
iOS 事件處理機制與圖像渲染過程
RunLoop學習筆記(一) 基本原理介紹
iOS刨根問底-深入理解RunLoop
【iOS程序啟動與運轉】- RunLoop個人小結
RunLoop的前世今生
Runloop知識樹