RunLoop是iOS和OSX中基本的概念,掌握RunLoop,能了解到蘋果是如何利用RunLoop實現(xiàn)自動釋放池、延遲回調(diào)、觸摸事件、屏幕刷新等功能的,并能在開發(fā)中優(yōu)化你的程序。
蘋果提供了兩個對象:NSRunLoop 和 CFRunLoopRef。
NSRunLoop是對CFRunLoopRef的封裝,提供了面向?qū)ο蟮腁PI,這些API并不是線程安全。
CFRunLoopRef提供C函數(shù)API,并且API都是線程安全的。
CFRunLoopRef源碼可以在這里下載閱讀。
RunLoop的概念
RunLoop可以理解為會在內(nèi)部卡住的do-while循環(huán),當(dāng)某個事件繼續(xù)驅(qū)動循環(huán)時,循環(huán)得以往下執(zhí)行或者退出。例如下面的偽代碼:
objectivec
do{
dothing();
sleep();
//代碼停在這一行,等待喚醒消息。
id message = wakeUpMessage();
doMessage(message);
}while(message != quit || != loopStop);
RunLoop的核心就是這個事件驅(qū)動循環(huán)(Event Loop)。在線程中執(zhí)行這個循環(huán)后,在一直處于循環(huán)內(nèi)部"等待->接受消息->處理消息->等待"狀態(tài),直到傳入了退出循環(huán)的消息或者循環(huán)退出。
##RunLoop的結(jié)構(gòu)
有五個類與RunLoop相關(guān):
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef

一個RunLoop中可以有多個Mode,一個Mode可以有多個Source/Timer/Observer。
###CFRunLoopSourceRef
soure包含兩個版本:
- soure0:處理App內(nèi)部事件,App自己負(fù)責(zé)管理這些時間,比如UIEvent和CFSocket。
- soure1:由RunLoop和內(nèi)核管理,由Mach port驅(qū)動,如CFMackPort、CFMessagePort,用于線程間通信。
###CFRunLoopTimerRef
我們開發(fā)中經(jīng)常使用的NSTimer就是來自于這里。當(dāng)其加入到RunLoop時,RunLoop會在相應(yīng)的時間點注冊好回調(diào),當(dāng)時間點到達(dá)時,RunLoop被喚醒以執(zhí)行回調(diào)。
###CFRunLoopObserverRef
observer是觀察者。每個Observer都包含一個回調(diào),用于當(dāng)RunLoop發(fā)生狀態(tài)變化時,觀察者能接收到相應(yīng)的回調(diào)??梢越邮盏降幕卣{(diào)有:
```objectivec```
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
};
CFRunLoopModeRef
結(jié)構(gòu)如下:
objectivec
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
};
每個Mode除了上面提到的sources/observer/timer外,還對應(yīng)著一個ModeName。蘋果公開提供的 Mode 有兩個:NSDefaultRunLoopMode和 UITrackingRunLoopMode,你可以用這兩個 Mode Name 來操作其對應(yīng)的 Mode。
需要注意的是RunLoop在同一時間內(nèi)只能且必須在一個Mode下運行,否則RunLoop無法運行。并且當(dāng)需要更換Mode時,需要停止RunLoop,重新賦值Mode,然后重新啟動RunLoop。
###RunLoop 的內(nèi)部邏輯

通過整理后的偽代碼如下:
```objectivec```
id runLoop{
//通過Observer通知,即將進(jìn)入Loop
runLoopObserver(kCFRunLoopEntry);
//進(jìn)入RunLoop內(nèi)部循環(huán)
runLoopRun(){
do{
//通過Observer通知,RunLoop 即將觸發(fā) Timer 回調(diào)。
runLoopObserver(kCFRunLoopBeforeTimers);
//通過Observer通知,RunLoop 即將觸發(fā) Source0回調(diào)。
runLoopObserver(kCFRunLoopBeforeSources);
doTimer();
doSource0();
//如果有source1消息,跳過睡眠
if(source1) goto handle_msg;
//通過Observer通知,RunLoop 的線程即將進(jìn)入休眠(sleep)。
runLoopObserver(kCFRunLoopBeforeWaiting);
//RunLoop等待被喚醒
//Z z z...
//通過Observer通知,RunLoop被喚醒。
runLoopObserver(kCFRunLoopAfterWaiting);
handle_msg:
//根據(jù)被喚醒的類型,處理消息
}while(!stop);
//通過Observer通知,RunLoop 即將退出。
runLoopObserver(kCFRunLoopExit);
}
}
RunLoop的應(yīng)用
滑動優(yōu)化
主線程的 RunLoop 里有兩個預(yù)置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。DefaultMode 是 App 平時所處的狀態(tài),TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態(tài)。將Timer或者其它消耗性能的回調(diào)注冊在kCFRunLoopDefaultMode,當(dāng)頁面進(jìn)行滑動時,都會停止這些回調(diào)。不會影響到滑動操作。
AutoreleasePool
當(dāng)即將進(jìn)入Loop,會創(chuàng)建AutoreleasePool,其優(yōu)先級最高,保證在發(fā)生其它回調(diào)之前。當(dāng)RunLoop即將進(jìn)入休眠時,會pop掉舊的AutoreleasePool,創(chuàng)建新的AutoreleasePool。最后RunLoop退出時,釋放AutoreleasePool,其優(yōu)先級最低,保證其釋放池發(fā)生在其他所有回調(diào)之后。其中的Autorelease對象在AutoreleasePool釋放后被釋放。
AFN中RunLoop的應(yīng)用
AFNetworking 單獨創(chuàng)建了一個線程,并在這個線程中啟動了一個 RunLoop:
objectivec
(void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}(NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
其中添加的[NSMachPort port]其實是一個空port,目的只是讓RunLoop不至于退出,達(dá)到線程保活。
###AsyncDisplayKit
我們知道,當(dāng)UI 線程中一旦出現(xiàn)繁重的任務(wù)就會導(dǎo)致界面卡頓。而AsyncDisplayKit的做法就是將這些任務(wù)在后臺執(zhí)行,并監(jiān)聽主線程的Observer。當(dāng)主線程RunLoop進(jìn)入 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit狀態(tài)時,遍歷所有之前放入隊列的待處理的任務(wù),然后一一執(zhí)行。
###最后
此文章概括得比較簡單,大多數(shù)內(nèi)容都是參考:
- [深入理解RunLoop](http://blog.ibireme.com/2015/05/18/runloop/)
- [iOS線下分享《RunLoop》by 孫源@sunnyxx](http://v.youku.com/v_show/id_XODgxODkzODI0.html)
再加上自己的一點筆記。有錯誤的地方希望大家可以留言改正,有什么問題也可以留言一起討論。