RunLoop 文章已經(jīng)很多了,結(jié)合各大文章做個總結(jié)
什么是 RunLoop
RunLoop 人如其名骡显,run 跑星爪,loop 循環(huán)浆西,無限的跑,類似于程序中的無限循環(huán)顽腾,RunLoop 是與線程相關(guān)聯(lián)的基礎(chǔ)架構(gòu)的一部分近零。 RunLoop 是一個事件處理循環(huán),用于調(diào)度和協(xié)調(diào)接收到的事件抄肖。 RunLoop 的目的是保持你的線程活著久信,在有消息到來的時候喚醒,沒有消息的時候休眠漓摩。
類似的機(jī)制在其他系統(tǒng)也存在
如在 Windows 中的 Message Loop
while(GetMessage(&Msg, NULL, 0, 0) > 0)
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
在安卓中的 MessageQueue 和 Looper
public class Looper {
public static final void loop() {
Looper me = myLooper();
MessageQueue queue = me.mQueue;
while (true) {
Message msg = queue.next();
if (msg != null) {
if (msg.target == null) {
return;
}
msg.target.dispatchMessage(msg);
msg.recycle();
}
}
}
}
為什么需要 RunLoop
一般來說一個線程一次只能執(zhí)行一個任務(wù)裙士,執(zhí)行完成后線程就會退出。對 APP 來說管毙,這顯然是不符合我們的要求的腿椎,沒有人希望 APP 的應(yīng)用在啟動之后就自動退出的。這時候就需要一個機(jī)制讓線程能隨時處理事件但并不退出锅风,如主線程酥诽,在我們啟動之后可以一直存在,當(dāng)交互發(fā)生皱埠,又可以去處理相關(guān)的事件肮帐。傳統(tǒng)的死循環(huán)似乎是可以滿足我們的要求,但是死循環(huán)會導(dǎo)致 CPU 一直空轉(zhuǎn)大量消耗系統(tǒng)性能,RunLoop 真是為了這種場景情況下而生训枢,在沒有收到消息的時候會讓線程睡眠以避免資源占用托修、在有消息到來時立刻被喚醒。
NSRunLoop 和 CFRunLoopRef
NSRunLoop 基于 CFRunLoopRef 封裝恒界。
CFRunLoopRef 是在 CoreFoundation 框架內(nèi)的睦刃,它提供了純 C 函數(shù)的 API,所有這些 API 都是線程安全的十酣。
NSRunLoop 提供了面向?qū)ο蟮?API涩拙,這些 API 不是線程安全的。
RunLoop 執(zhí)行的流程
這邊用 ibireme 博客的圖片和代碼來說明 RunLoop 執(zhí)行的過程
內(nèi)部代碼
/// 用DefaultMode啟動
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
/// 用指定的Mode啟動耸采,允許設(shè)置RunLoop超時時間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
/// RunLoop的實現(xiàn)
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根據(jù)modeName找到對應(yīng)mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里沒有source/timer/observer, 直接返回兴泥。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1. 通知 Observers: RunLoop 即將進(jìn)入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 內(nèi)部函數(shù)虾宇,進(jìn)入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即將觸發(fā) Timer 回調(diào)搓彻。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即將觸發(fā) Source0 (非port) 回調(diào)。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 執(zhí)行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 觸發(fā) Source0 (非port) 回調(diào)嘱朽。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 執(zhí)行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 如果有 Source1 (基于port) 處于 ready 狀態(tài)旭贬,直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的線程即將進(jìn)入休眠(sleep)搪泳。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 調(diào)用 mach_msg 等待接受 mach_port 的消息稀轨。線程將進(jìn)入休眠, 直到被下面某一個事件喚醒。
/// ? 一個基于 port 的Source 的事件岸军。
/// ? 一個 Timer 到時間了
/// ? RunLoop 自身的超時時間到了
/// ? 被其他什么調(diào)用者手動喚醒
__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 到時間了,觸發(fā)這個Timer的回調(diào)脏榆。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block猖毫,執(zhí)行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一個 Source1 (基于port) 發(fā)出事件了须喂,處理這個事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 執(zhí)行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 進(jìn)入loop時參數(shù)說處理完事件就返回吁断。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出傳入?yún)?shù)標(biāo)記的超時時間了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部調(diào)用者強(qiáng)制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一個都沒有了
retVal = kCFRunLoopRunFinished;
}
/// 如果沒超時,mode里沒空坞生,loop也沒被停止仔役,那繼續(xù)loop。
} while (retVal == 0);
}
/// 10. 通知 Observers: RunLoop 即將退出是己。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
RunLoop 之 mode
熟悉 NSTimer 的同學(xué)一定知道當(dāng)使用 scheduledTimerWithTimeInterval 系列 API 的時候在 UIScrollView 以及其子類滾動的時候是不會調(diào)用回調(diào)又兵,如果我們需要要在滾動過程中也可以調(diào)用回調(diào)回調(diào)那就應(yīng)該這么寫
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(repeat:) userInfo:nil repeats:true];
[[NSRunLoop currentRunLoop] addTimer:timer forMode: NSRunLoopCommonModes];
為什么這邊涉及到了 RunLoop ?這里的 NSRunLoopCommonModes 又是什么玩意?
這是于 RunLoop 運(yùn)行的機(jī)制有關(guān)沛厨,一個 RunLoop 的結(jié)構(gòu)大概如下
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
可以看出宙地,在 RunLoop 執(zhí)行的時候總是會處在某一個 mode 狀態(tài)下。如剛進(jìn)入 APP 的時候啥事都沒干逆皮,這個時候 RunLoop 運(yùn)行的 mode 是 NSDefaultRunLoopMode(kCFRunLoopDefaultMode)宅粥,如果頁面有個 tableView 你滑動之后這個時候 RunLoop 會切換到 UITrackingRunLoopMode 模式下。NSTimer 的 scheduledTimerWithTimeInterval 系列的 API 默認(rèn)是將 timer 添加到 NSDefaultRunLoopMode 下的电谣,所以秽梅,當(dāng)你滑動 ScrollView 的時候切換了 mode ,當(dāng)然不能正常會調(diào)你的方法了剿牺。
RunLoop mode 的種類
RunLoop 系統(tǒng)預(yù)置的 mode 大概有幾個常用的
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默認(rèn)模式企垦。
- UITrackingRunLoopMode:滑動模式,在滑動 ScrollView 的時候會在此模式牢贸。
- NSRunLoopCommonModes:通用模式竹观,在滑動 ScrollView 和默認(rèn)狀態(tài)都會響應(yīng)事件。
CommonModes
一個 Mode 可以將自己設(shè)置為"CommonMode"(通過將其 ModeName 添加到 RunLoop 的 "commonModes" 中)潜索。每當(dāng) RunLoop 的內(nèi)容發(fā)生變化時臭增,RunLoop 都會將事件同步到 CommonMode 的 mode item,什么是 mode item 后文會講到竹习。這也解釋了為什么 timer 加入到 NSRunLoopCommonModes 中會被正確的回調(diào)誊抛。
管理mode
在 NSRunLoop 這個層面不提供操作 mode 的 API ,在 CFRunLoopRef 提供相關(guān)的管理 API,不過只提供了增加 mode 的方法,不提供刪除的方法整陌。
// 添加一個 CommonMode
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
//是否在 某個 mode 下
CFRunLoopRunInMode(CFStringRef modeName, ...);
RunLoop 之 mode item
上文的 RunLoop 流程圖中我們可以看到 Source0拗窃, Source1,Timer泌辫,Observer 随夸,這些都是 mode item。
上一小節(jié)震放,介紹了 RunLoop mode宾毒,我們可以看看 mode 的結(jié)構(gòu)
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
結(jié)合圖片理解
可以看出,一個 mode 由 _sources0殿遂,_sources1诈铛,_observers,_timers構(gòu)成墨礁。在也是每個 RunLoop 周期醒來之后要干的活幢竹。
Source0 只包含了一個回調(diào)(函數(shù)指針),它并不能主動觸發(fā)事件恩静。使用時焕毫,你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個 Source 標(biāo)記為待處理,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop咬荷,讓其處理這個事件冠句。
Source1 包含了一個 mach_port 和一個回調(diào)(函數(shù)指針),被用于通過內(nèi)核和其他線程相互發(fā)送消息幸乒。這種 Source 能主動喚醒 RunLoop 的線程懦底。
timer 是基于時間的觸發(fā)器,它和 NSTimer 是toll-free bridged 的罕扎,可以混用聚唐。其包含一個時間長度和一個回調(diào)(函數(shù)指針)。當(dāng)其加入到 RunLoop 時腔召,RunLoop會注冊對應(yīng)的時間點(diǎn)杆查,當(dāng)時間點(diǎn)到時,RunLoop會被喚醒以執(zhí)行那個回調(diào)臀蛛。
是觀察者亲桦,每個 Observer 都包含了一個回調(diào)(函數(shù)指針),當(dāng) RunLoop 的狀態(tài)發(fā)生變化時浊仆,觀察者就能通過回調(diào)接受到這個變化客峭。可以觀測的時間點(diǎn)有以下幾個:
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
};
一個 item 可以被同時加入多個 mode抡柿。但一個 item 被重復(fù)加入同一個 mode 時是不會有效果的舔琅。如果一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出洲劣,不進(jìn)入循環(huán)备蚓。
更詳細(xì)的內(nèi)容可以查看博客:http://blog.ibireme.com/2015/05/18/runloop/
RunLoop 啟動和退出
啟動
這邊以 NSRunLoop 為例,CFRunLoopRef 類似囱稽,
啟動有3個方法
- run
- runUntilDate:
- runMode:beforeDate:
run
底層是不斷(循環(huán))調(diào)用runMode:beforeDate:
來達(dá)到運(yùn)行目的郊尝。
runUntilDate:
底層也是調(diào)用runMode:beforeDate:
來運(yùn)行,和run
不同的是战惊,在指定的時間也就是 UntilDate 參數(shù)后會停止調(diào)用虚循。
退出
在系統(tǒng)提供的停止 RunLoop 方法只有 CFRunLoopStop()
,CFRunLoopStop()
方法只會結(jié)束當(dāng)前的 RunLoop 調(diào)用样傍,而不會結(jié)束后續(xù)的調(diào)用。也就意味著 如果你是用方法一也就是 run
的方式啟動 RunLoop铺遂,那么這個 RunLoop 不會被退出衫哥,因為它會不斷的啟動,因為run
底層是不斷(循環(huán))調(diào)用runMode:beforeDate:
來達(dá)到運(yùn)行目的襟锐。如果你是使用runUntilDate:
啟動的撤逢,那么超時結(jié)束后會自動終止 RunLoop,如果是runMode:beforeDate:
那么你可以精確的控制 RunLoop 的停止。
RunLoop 應(yīng)用
RunLoop 用途廣泛蚊荣,如 AutoreleasePool初狰,PerformSelecter,GCD互例,AsyncDisplayKit等都有涉及到奢入,更多神秘等著我們?nèi)ヌ剿鳌?/p>
后話
根據(jù) RunLoop 我們可以完成一個主線程卡頓的監(jiān)控工具,在計劃中媳叨,完成后會貼出地址腥光。