我們知道NSTimer
是不精準(zhǔn)的配乓,如果NSTimer
當(dāng)前所處的線程正在進行大數(shù)據(jù)處理(假設(shè)為一個大循環(huán)),NSTimer
本次執(zhí)行會等到這個大數(shù)據(jù)處理完畢之后才會繼續(xù)執(zhí)行。這是因為我們創(chuàng)建的NSTimer
默認(rèn)是被主線程添加到NSDefaultRunLoopMode模式下,一般情況,我們?nèi)绻胍?code>NSTimer準(zhǔn)確沉衣,可以將其添加到NSRunLoopCommonModes:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
那么問題就來了,為什么能這么做呢楷掉?這就是我們今天的主角RunLoop了厢蒜。
基本概念
RunLoop 與線程的關(guān)系
接口及其聯(lián)系
調(diào)用流程
-
應(yīng)用
1. 基本概念
RunLoop是與線程相關(guān)的基本基礎(chǔ)設(shè)施的一部分霞势。一個RunLoop是一個事件處理循環(huán),用于調(diào)度工作并協(xié)調(diào)傳入事件的接收斑鸦。愕贡。目的是在有工作要做的時候保持線程的忙碌,閑置的情況下讓線程休眠巷屿。
一般來講固以,一個線程一次只能執(zhí)行一個任務(wù),執(zhí)行完成后線程就會退出嘱巾。如果我們需要一個機制憨琳,讓線程能隨時處理事件但并不退出,通常的代碼邏輯是這樣的:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
這種模型通常被稱作 Event Loop 旬昭。RunLoop就是采用的這種機制(通常所說的RunLoop指的是NSRunloop或者CFRunloopRef篙螟,CFRunloopRef是純C的函數(shù),而NSRunloop僅僅是CFRunloopRef的OC封裝问拘,并未提供額外的其他功能)遍略,安卓里面的Looper機制和此類似≈枳可以理解為一種高級的循環(huán)绪杏,當(dāng)有事件發(fā)生時, RunLoop 會去找對應(yīng)的 Handler 處理事件纽绍;當(dāng)沒有事件時蕾久,RunLoop 會進入休眠狀態(tài)。如下圖拌夏,可以看出他的工作流程:從 input source 和 timer source 中接受到事件僧著,然后在線程中去處理事件。
Input Source 和 Timer Source
這兩個都是 RunLoop 事件的來源辖佣,Input Source 傳送來自其他應(yīng)用或線程的異步事件/消息霹抛;Input Source 又可以分為三類(按照函數(shù)調(diào)用棧又可分為Source0和Source1,見CFRunLoopSourceRef)卷谈。
- Port-Based Sources,系統(tǒng)底層的 Port 事件霞篡,例如 CFSocketRef
- Custom Input Sources世蔗,用戶手動創(chuàng)建的 Source
- Cocoa Perform Selector Sources, Cocoa 提供的 performSelector 系列方法朗兵,也是一種事件源
Timer Source 傳送的是基于定時器的同步事件污淋,可以定時或重復(fù)發(fā)送。
2. RunLoop 與線程的關(guān)系
蘋果不允許直接創(chuàng)建 RunLoop余掖,但是提供了兩個自動獲取的函數(shù):CFRunLoopGetMain() 和 CFRunLoopGetCurrent()寸爆。 這兩個函數(shù)內(nèi)部的邏輯大概是下面這樣:
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
/// 獲取一個 pthread 對應(yīng)的 RunLoop赁豆。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次進入時仅醇,初始化全局Dic,并先為主線程創(chuàng)建一個 RunLoop魔种。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接從 Dictionary 里獲取析二。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到時,創(chuàng)建一個
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注冊一個回調(diào)节预,當(dāng)線程銷毀時叶摄,順便也銷毀其對應(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());
}
結(jié)論:線程和 RunLoop 之間是一一對應(yīng)的安拟,其關(guān)系是保存在一個全局的 Dictionary 里蛤吓。線程剛創(chuàng)建時并沒有 RunLoop,如果你不主動獲取糠赦,那它一直都不會有会傲。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時,RunLoop 的銷毀是發(fā)生在線程結(jié)束時愉棱。你只能在一個線程的內(nèi)部獲取其 RunLoop(主線程除外)唆铐。
3. 接口及其聯(lián)系
在 CoreFoundation/CFRunLoop中,定義了5個struct:
CFRunLoopMode
CFRunLoopRef
CFRunLoopSourceRef
CFRunLoopObserverRef
CFRunLoopTimerRef
線程和 RunLoop 是一一對應(yīng)的奔滑,一個 RunLoop 包含若干個 Mode艾岂,每個 Mode 又包含若干個 Source/Timer/Observer,Source又按照事件分為Source0 和 Source1朋其。
每次調(diào)用 RunLoop 的主函數(shù)時王浴,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode梅猿。如果需要切換 Mode氓辣,只能退出 Loop,再重新指定一個 Mode 進入袱蚓。這樣做主要是為了分隔開不同組的 Source/Timer/Observer钞啸,讓其互不影響。
如下圖:
1. CFRunLoopMode:
系統(tǒng)默認(rèn)注冊了五中Mode:
- NSDefaultRunLoopMode:(kCFRunLoopDefaultMode (Core Foundation)) App的默認(rèn)Mode,通常主線程實在這個模式下運行
- UITrackingRunLoopMode:界面跟蹤Mode,用于界面控件(ScrollView,tableView等等)追蹤觸摸滑動,保證界面滑動時不受其他Mode影響
- UIInitializationRunLoopMode:在剛啟動App是進入的第一個Mode,啟動完成后就不再使用
- GSEventReceiveRunLoopMode:接收系統(tǒng)事件的內(nèi)部Mode,通常用不到
- NSRunLoopCommonMode:這是一個占位的Mode,不是一種真正的Mode,(可以看成模式組,默認(rèn)情況下包括了NSDefaultRunLoopMode,UITrackingRunLoopMode)兩種模式.
iOS 中暴露出來的只有 NSDefaultRunLoopMode 和 NSRunLoopCommonModes喇潘。 NSRunLoopCommonModes 實際上是一個 Mode 的集合体斩,默認(rèn)包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode。
2. CFRunLoopRef:
RunLoop對象颖低,NSRunLoop是基于CFRunLoopRef的一層OC包裝絮吵。
3. CFRunLoopSourceRef:
是事件產(chǎn)生的地方。Source又分為2種:Source0 和 Source1忱屑。
- Source0 是非基于 port 的事件蹬敲,主要是 APP 內(nèi)部事件暇昂,如點擊事件,觸摸事件等伴嗡。只包含了一個回調(diào)(函數(shù)指針)急波,它并不能主動觸發(fā)事件。使用時闹究,你需要先調(diào)用 CFRunLoopSourceSignal(source)幔崖,將這個 Source 標(biāo)記為待處理,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop渣淤,讓其處理這個事件赏寇。
- Source1 是基于Port的,通過內(nèi)核和其他線程通信价认,接收嗅定,分發(fā)系統(tǒng)事件。包含了一個 mach_port 和一個回調(diào)(函數(shù)指針)用踩,被用于通過內(nèi)核和其他線程相互發(fā)送消息渠退。這種 Source 能主動喚醒 RunLoop 的線程。
4. CFRunLoopObserverRef:
每個 Observer 都包含了一個回調(diào)(函數(shù)指針)脐彩,當(dāng) RunLoop 的狀態(tài)發(fā)生變化時碎乃,觀察者就能通過回調(diào)接受到這個變化。
RunLoop狀態(tài)可分為:
- kCFRunLoopEntry 即將進入runLoop
- kCFRunLoopBeforeTimers 即將處理Timer
- kCFRunLoopBeforeSources 即將處理source(事件源)
- kCFRunLoopBeforeWaiting 即將進入休眠
- kCFRunLoopAfterWaiting 被喚醒但是還沒開始處理事件
- kCFRunLoopExit 即將退出runLoop
5. CFRunLoopTimerRef:
CFRunLoopTimerRef是基于時間的觸發(fā)器惠奸,其包含一個時間長度和一個回調(diào)(函數(shù)指針)梅誓。當(dāng)其加入到 RunLoop 時,RunLoop會注冊對應(yīng)的時間點佛南,當(dāng)時間點到時梗掰,RunLoop會被喚醒以執(zhí)行那個回調(diào)。
-
CFRunLoopTimerRef基本上說的就是NSTimer,它受RunLoop的Mode影響
4. 調(diào)用流程
5. 應(yīng)用
1. AutoreleasePool
當(dāng)App啟動之后嗅回,系統(tǒng)會啟動主線程并創(chuàng)建RunLoop及穗,在 main thread 中注冊了兩個 observer ,回調(diào)都是
_wrapRunLoopWithAutoreleasePoolHandler()
绵载,分別會在進去Loop的kCFRunLoopEntry狀態(tài)回調(diào)調(diào)用_objc_autoreleasePoolPush
方法創(chuàng)建AutoreleasePool;在kCFRunLoopBeforeWaiting進入休眠時調(diào)用_objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
來釋放舊的池并創(chuàng)建新的池埂陆;以及在kCFRunLoopExit退出時釋放。
2. 事件響應(yīng)
系統(tǒng)注冊了一個 Source1 用于接收系統(tǒng)事件娃豹,其回調(diào)函數(shù)為
__IOHIDEventSystemClientQueueCallback()
猜惋。當(dāng)一個硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由 IOKit.framework 生成一個 IOHIDEvent 事件并由 SpringBoard 接收培愁。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸缓窜,加速定续,接近傳感器等幾種 Event谍咆,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進程。隨后蘋果注冊的那個 Source1 就會觸發(fā)回調(diào)私股,并調(diào)用 _UIApplicationHandleEventQueue() 進行應(yīng)用內(nèi)部的分發(fā)摹察。_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理并包裝成 UIEvent 進行處理或分發(fā),其中包括識別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow 等倡鲸。通常事件比如 UIButton 點擊供嚎、touchesBegin/Move/End/Cancel 事件都是在這個回調(diào)中完成的。
3. 手勢識別
當(dāng)上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時峭状,其首先會調(diào)用 Cancel 將當(dāng)前的 touchesBegin/Move/End 系列回調(diào)打斷克滴。隨后系統(tǒng)將對應(yīng)的 UIGestureRecognizer 標(biāo)記為待處理。
蘋果注冊了一個 Observer 監(jiān)測 BeforeWaiting (Loop即將進入休眠) 事件优床,這個Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver()劝赔,其內(nèi)部會獲取所有剛被標(biāo)記為待處理的 GestureRecognizer,并執(zhí)行GestureRecognizer的回調(diào)胆敞。
當(dāng)有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時着帽,這個回調(diào)都會進行相應(yīng)處理。
4. 界面更新
當(dāng)在操作 UI 時移层,比如改變了 Frame仍翰、更新了 UIView/CALayer 的層次時,或者手動調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后观话,這個 UIView/CALayer 就被標(biāo)記為待處理予借,并被提交到一個全局的容器去友瘤。
蘋果注冊了一個 Observer 監(jiān)聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件紫谷,回調(diào)去執(zhí)行一個很長的函數(shù):
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個函數(shù)里會遍歷所有待處理的 UIView/CAlayer 以執(zhí)行實際的繪制和調(diào)整避矢,并更新 UI 界面帽驯。
這個函數(shù)內(nèi)部的調(diào)用棧大概是這樣的:
_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];
5. 定時器
NSTimer 其實就是 CFRunLoopTimerRef龟再,他們之間是 toll-free bridged 的。一個 NSTimer 注冊到 RunLoop 后尼变,RunLoop 會為其重復(fù)的時間點注冊好事件利凑。例如 10:00, 10:10, 10:20 這幾個時間點。RunLoop為了節(jié)省資源嫌术,并不會在非常準(zhǔn)確的時間點回調(diào)這個Timer哀澈。Timer 有個屬性叫做 Tolerance (寬容度),標(biāo)示了當(dāng)時間點到后度气,容許有多少最大誤差割按。
如果某個時間點被錯過了,例如執(zhí)行了一個很長的任務(wù)磷籍,則那個時間點的回調(diào)也會跳過去适荣,不會延后執(zhí)行现柠。就比如等公交,如果 10:10 時我忙著玩手機錯過了那個點的公交弛矛,那我只能等 10:20 這一趟了够吩。
CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現(xiàn)原理更復(fù)雜,和 NSTimer 并不一樣丈氓,其內(nèi)部實際是操作了一個 Source)周循。如果在兩次屏幕刷新之間執(zhí)行了一個長任務(wù),那其中就會有一幀被跳過去(和 NSTimer 相似)万俗,造成界面卡頓的感覺湾笛。
NSTimer的創(chuàng)建通常有兩種方式,盡管都是類方法该编,一種是timerWithXXX迄本,另一種scheduedTimerWithXXX。二者最大的區(qū)別就是后者除了創(chuàng)建一個定時器外會自動以NSDefaultRunLoopMode添加到當(dāng)前線程RunLoop中课竣,不添加到RunLoop中的NSTimer是無法正常工作的嘉赎。
6. performSelecter
當(dāng)調(diào)用 NSObject 的 performSelecter:afterDelay: 后,實際上其內(nèi)部會創(chuàng)建一個 Timer 并添加到當(dāng)前線程的 RunLoop 中于樟。所以如果當(dāng)前線程沒有 RunLoop公条,則這個方法會失效。
當(dāng)調(diào)用 performSelector:onThread: 時迂曲,實際上其會創(chuàng)建一個 Timer 加到對應(yīng)的線程去靶橱,同樣的,如果對應(yīng)線程沒有 RunLoop 該方法也會失效路捧。
7. RunLoop 與 GCD
RunLoop 與 GCD 是互相協(xié)作的關(guān)系关霸,RunLoop 的最開始部分使用了 GCD 的 timer 做超時的回調(diào);通過 GCD 調(diào)用帶有 RunLoop 的線程的 block杰扫,會通過
dispatch port CFRunLoopServiceMachPort
把事件發(fā)送到該線程的 RunLoop 里面队寇。
比如:
dispatch_async(dispatch_get_main_queue(), ^{});
主線程存在 RunLoop,那么 GCD 會通過
dispatch port CFRunLoopServiceMachPort
章姓,把事件發(fā)送給 RunLoop佳遣,RunLoop 接收到事件后,會執(zhí)行這個 block凡伊。但這個邏輯僅限于 dispatch 到主線程零渐,dispatch 到其他線程仍然是由 libDispatch 處理的。
6. 使用RunLoop
在次級線程運行 run loop 之前系忙,必須向其添加至少一個 input source 或 timer诵盼,否則 run loop 會因沒有可監(jiān)控的 source 而在運行后立刻退出。
除了用 source 外,還可以用 run loop observer 觀察 run loop 的各種運行階段拦耐。做法是創(chuàng)建一個 CFRunLoopObserverRef
類型的對象并用 CFRunLoopAddObserver
函數(shù)將其添加到 run loop 中耕腾。注意的是只能用 Core Foundation 創(chuàng)建 run loop observer,Cocoa 框架無能為力杀糯。
下面的示例代碼在線程入口函數(shù)中創(chuàng)建了 run loop observer 并將其添加到 run loop 中。observer 監(jiān)聽了 run loop 所有的活動苍苞,并省略了回調(diào)函數(shù) myRunLoopObserver
的實現(xiàn)固翰。
- (void)threadMain
{
// The application uses garbage collection, so no autorelease pool is needed.
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
// Create a run loop observer and attach it to the run loop.
CFRunLoopObserverContext context = {0, self, NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
if (observer)
{
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}
// Create and schedule the timer.
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self
selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
NSInteger loopCount = 10;
do
{
// Run the run loop 10 times to let the timer fire.
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;
}
while (loopCount);
}
為了不讓 run loop 剛運行就立刻退出,上面的代碼向 run loop 添加了一個 timer羹呵。因為 timer 一旦觸發(fā)就無效了骂际,依然會導(dǎo)致 run loop 退出,所以這里 repeats
參數(shù)傳入 YES
冈欢。但這樣會讓 run loop 一直運行很久歉铝,并需要周期性觸發(fā) timer 來喚醒線程,這實際上是輪詢的另一種形式罷了凑耻。相比之下太示,input source 等待事件發(fā)生后才喚醒線程,在這之前線程保持休眠香浩。
參考文獻