剛剛開始學(xué)習(xí)IOS的時候,聽說過Runloop但是一直不知道是什么東西,如何使用,最近在學(xué)習(xí)相關(guān)的知識,但是當我準備寫一篇最近的學(xué)習(xí)心得的時候。發(fā)現(xiàn)了一位大牛寫的文章,頓時放棄了自己總結(jié)的想法。但是這位大牛寫的文章中,對于Runloop的使用范例只列舉了AFNetworking中是如何使用的,所以我準備對Runloop的實際使用進行更多的舉例。
RunLoop 的概念
一般來講,一個線程一次只能執(zhí)行一個任務(wù),執(zhí)行完成后線程就會退出。如果我們需要一個機制,讓線程能隨時處理事件但并不退出,通常的代碼邏輯是這樣的:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
這種模型通常被稱作 Event Loop。 Event Loop 在很多系統(tǒng)和框架里都有實現(xiàn),比如 Node.js 的事件處理,比如 Windows 程序的消息循環(huán),再比如 OSX/iOS 里的 RunLoop。實現(xiàn)這種模型的關(guān)鍵點在于:如何管理事件/消息,如何讓線程在沒有處理消息時休眠以避免資源占用、在有消息到來時立刻被喚醒。
所以,RunLoop 實際上就是一個對象,這個對象管理了其需要處理的事件和消息,并提供了一個入口函數(shù)來執(zhí)行上面 Event Loop 的邏輯。線程執(zhí)行了這個函數(shù)后,就會一直處于這個函數(shù)內(nèi)部 "接受消息->等待->處理" 的循環(huán)中,直到這個循環(huán)結(jié)束(比如傳入 quit 的消息),函數(shù)返回。
OSX/iOS 系統(tǒng)中,提供了兩個這樣的對象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架內(nèi)的,它提供了純 C 函數(shù)的 API,所有這些 API 都是線程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封裝,提供了面向?qū)ο蟮?API,但是這些 API 不是線程安全的。
CFRunLoopRef 的代碼是開源的,你可以在這里 http://opensource.apple.com/tarballs/CF/CF-855.17.tar.gz 下載到整個 CoreFoundation 的源碼。為了方便跟蹤和查看,你可以新建一個 Xcode 工程,把這堆源碼拖進去看。
RunLoop 與線程的關(guān)系
首先,iOS 開發(fā)中能遇到兩個線程對象: pthread_t 和 NSThread。過去蘋果有份文檔標明了 NSThread 只是 pthread_t 的封裝,但那份文檔已經(jīng)失效了,現(xiàn)在它們也有可能都是直接包裝自最底層的 mach thread。蘋果并沒有提供這兩個對象相互轉(zhuǎn)換的接口,但不管怎么樣,可以肯定的是 pthread_t 和 NSThread 是一一對應(yīng)的。比如,你可以通過 pthread_main_np() 或 [NSThread mainThread] 來獲取主線程;也可以通過 pthread_self() 或 [NSThread currentThread] 來獲取當前線程。CFRunLoop 是基于 pthread 來管理的。
蘋果不允許直接創(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),當線程銷毀時,順便也銷毀其對應(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());
}
從上面的代碼可以看出,線程和 RunLoop 之間是一一對應(yīng)的,其關(guān)系是保存在一個全局的 Dictionary 里。線程剛創(chuàng)建時并沒有 RunLoop,如果你不主動獲取,那它一直都不會有。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時,RunLoop 的銷毀是發(fā)生在線程結(jié)束時。你只能在一個線程的內(nèi)部獲取其 RunLoop(主線程除外)。
RunLoop 的內(nèi)部邏輯
根據(jù)蘋果在文檔里的說明,RunLoop 內(nèi)部的邏輯大致如下:
其內(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 即將進入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 內(nèi)部函數(shù),進入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 的線程即將進入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 調(diào)用 mach_msg 等待接受 mach_port 的消息。線程將進入休眠, 直到被下面某一個事件喚醒。
/// ? 一個基于 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) {
/// 進入loop時參數(shù)說處理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出傳入?yún)?shù)標記的超時時間了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部調(diào)用者強制停止了
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 就是這樣一個函數(shù),其內(nèi)部是一個 do-while 循環(huán)。當你調(diào)用 CFRunLoopRun() 時,線程就會一直停留在這個循環(huán)里;直到超時或被手動停止,該函數(shù)才會返回。
Run Loop Modes
理解
Run Loop Mode就是流水線上支持生產(chǎn)的產(chǎn)品類型,流水線在一個時刻只能在一種模式下運行,生產(chǎn)某一類型的產(chǎn)品。消息事件就是訂單。
Cocoa定義了四中Mode
Default:NSDefaultRunLoopMode,默認模式,在Run Loop沒有指定Mode的時候,默認就跑在Default Mode下
Connection:NSConnectionReplyMode,用來監(jiān)聽處理網(wǎng)絡(luò)請求NSConnection的事件
Modal:NSModalPanelRunLoopMode,OS X的Modal面板事件
Event tracking:UITrackingRunLoopMode,拖動事件
Common mode:NSRunLoopCommonModes,是一個模式集合,當綁定一個事件源到這個模式集合的時候就相當于綁定到了集合內(nèi)的每一個模式
RunLoop可以通過[acceptInputForMode:beforeDate:]和[runMode:beforeDate:]來指定在一段時間內(nèi)的運行模式。如果不指定的話,RunLoop默認會運行在Default下(不斷重復(fù)調(diào)用runMode:NSDefaultRunLoopMode beforDate:)
在主線程啟動一個計時器Timer,然后拖動UITableView或者UIScrollView,計時器不執(zhí)行。這是因為,為了更好的用戶體驗,在主線程中Event tracking模式的優(yōu)先級最高。在用戶拖動控件時,主線程的Run Loop是運行在Event tracking Mode下,而創(chuàng)建的Timer是默認關(guān)聯(lián)為Default Mode,因此系統(tǒng)不會立即執(zhí)行Default Mode下接收的事件。解決方法:
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(timerFireMethod:)
userInfo:nil
repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
//或
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
[timer fire];
Run Loop特性
主線程的RunLoop在應(yīng)用啟動的時候就會自動創(chuàng)建
其他線程則需要在該線程下自己啟動
不能自己創(chuàng)建RunLoop
RunLoop并不是線程安全的,所以需要避免在其他線程上調(diào)用當前線程的RunLoop
RunLoop負責管理autorelease pools
RunLoop負責處理消息事件,即輸入源事件和計時器事件
Run Loop應(yīng)用實踐(一)
在一定時間內(nèi)監(jiān)聽某種事件,或執(zhí)行某種任務(wù)的線程
如下代碼,在30分鐘內(nèi),每隔30s執(zhí)行onTimerFired:。這種場景一般會出現(xiàn)在,如我需要在應(yīng)用啟動之后,在一定時間內(nèi)持續(xù)更新某項數(shù)據(jù)。
@autoreleasepool {
NSRunLoop * runLoop = [NSRunLoop currentRunLoop];
NSTimer * udpateTimer = [NSTimer timerWithTimeInterval:30
target:self
selector:@selector(onTimerFired:)
userInfo:nil
repeats:YES];
[runLoop addTimer:udpateTimer forMode:NSRunLoopCommonModes];
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60*30]];
}
AFNetworking中RunLoop的創(chuàng)建
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 這里主要是監(jiān)聽某個 port,目的是讓這個 Thread 不會回收
[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;
}
創(chuàng)建常駐線程,執(zhí)行一些會一直存在的任務(wù)。該線程的生命周期跟App相同
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
文章中設(shè)計到的代碼已經(jīng)上傳至github上