RunLoop 是什么
強烈推薦
ibireme
大神的文章深入理解RunLoop
關(guān)于 Runloop
引镊,盡管早就知道它的本質(zhì)實現(xiàn)是一個循環(huán),但筆者還是一直很困惑它的作用是什么 ,不過最近整理相關(guān)知識總算是理解了。
代碼的執(zhí)行邏輯是自上而下的,如果沒有 Runloop
希坚,代碼執(zhí)行完畢后,程序就退出了陵且,對應(yīng)到實際場景就是 APP
一打開立馬就退出了裁僧。
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"程序執(zhí)行中...");
}
return 0;
}
// log
程序執(zhí)行中...
Program ended with exit code: 0
例如上面的代碼,代碼執(zhí)行完畢后慕购,main
函數(shù)返回聊疲,然后程序退出。
為什么工作中沪悲,好像沒有編寫 Runloop
相關(guān)的代碼售睹,程序還是能夠穩(wěn)定持續(xù)運行呢?
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
這是因為程序自動幫我們在 UIApplicationMain…
中做了這個事情可训。
下面來看看 Runloop
的簡化的偽代碼昌妹,主要來自 sunnyxx 大神的一次視頻分享:
function loop() {
do {
有事干了 = 我睡覺了沒事別找我()捶枢;
if (搬磚) {
搬磚();
} else if (吃飯) {
吃飯()飞崖;
}
} while (活著)
}
這個偽代碼看著還是有一點抽象烂叔,需要了解的一個知識點是線程和 RunLoop
之間是一一對應(yīng)的,這里的睡覺了可以理解為線程休眠 [NSThread sleepUntilDate:...]]
固歪,也就是說當(dāng)應(yīng)用沒有任何事件觸發(fā)時蒜鸡,就會停在睡覺那行代碼不執(zhí)行,這樣就節(jié)約了 CPU
的運算資源牢裳,提高程序性能逢防,直到有事件喚醒應(yīng)用為止。例如上面的搬磚事件蒲讯,吃飯事件忘朝。處理完后,又會進(jìn)入睡覺狀態(tài)直到下次喚醒判帮,反復(fù)循環(huán)局嘁,這樣就保證了程序能隨時處理各種事件并能夠穩(wěn)定運行。
實際上觸摸事件晦墙、屏幕 UI
刷新悦昵、延遲回調(diào)等等都是 Runloop
實現(xiàn)的。
Runloop 的結(jié)構(gòu)
先來看看 Runloop
的結(jié)構(gòu)源碼:
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
// ...
};
這里包含一個線程的成員變量 _pthread
晌畅,可以看出 Runloop
確實和線程是息息相關(guān)的但指。還能看到 Runloop
擁有很多關(guān)于 Model
的成員變量,再來看看 Model
的結(jié)構(gòu):
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
// ...
};
先不管這些東西是干什么的抗楔,至少我們現(xiàn)在能夠得出如下圖所示的理解:
一個 Runloop
中包含若干個 Model
棋凳,每個 Mode
又包含若干個 Source/Timer/Observer
。
Runloop 的 Model
Model
代表 Runloop
的運行模式谓谦,Runloop
每次只能指定一個 Model
作為 _currentMode
,如果需要切換 Mode
贪婉,只能退出當(dāng)前 Loop
反粥,再重新選擇一個 Mode
進(jìn)入。主線程的 Runloop
這里有兩個預(yù)置的模式 疲迂,并且這也是系統(tǒng)公開的兩個 Model
:
kCFRunLoopDefaultMode:
APP
的普通狀態(tài)才顿,通常主線程是在這個Mode下運行,已被標(biāo)記為Common
尤蒿。UITrackingRunLoopMode:
App
追蹤觸摸ScrollView
滑動時的狀態(tài)郑气,保證界面滑動時不受其他Mode
影響,已被標(biāo)記為Common
腰池。
注意 Runloop
的結(jié)構(gòu)中有一個 _commonModes
尾组。這里是因為一個 Mode
可以將自己標(biāo)記為 Common
(通過將其 ModeName
添加到 RunLoop
的 commonModes
中 )忙芒,標(biāo)記為 Common
的 Model
都可以處理事件,可以理解為變相的實現(xiàn)了多個 Model
同時運行讳侨。同時系統(tǒng)也提供了一個操作 Common
標(biāo)記的字符串->kCFRunLoopCommonModes
呵萨。如果我們想要上面兩種模式下都能處理事件,就可以使用這個字符串跨跨。
Model 中的 Item
Source/Timer/Observer
被統(tǒng)稱為 mode item潮峦,不同 Model
的 Source0/Source1/Timer/Observer
被分隔開來,互不影響勇婴,如果 Mode
里沒有任何Source0/Source1/Timer/Observer
忱嘹,RunLoop
會立馬退出。
Source
Source
是事件產(chǎn)生的的地方耕渴,它對應(yīng)的類為 CFRunLoopSourceRef
拘悦。Source
有兩個版本:Source0
和 Source1
。
-
Source0
只包含了一個回調(diào)(函數(shù)指針)萨螺,它并不能主動觸發(fā)事件窄做。 -
Source1
包含了一個mach_port
和一個回調(diào)(函數(shù)指針),被用于通過內(nèi)核和其他線程相互發(fā)送消息慰技。這種Source
能主動喚醒RunLoop
的線程椭盏。例如屏幕觸摸、鎖屏和搖晃等吻商。
Timer
Timer
對應(yīng)的類是 CFRunLoopTimerRef
掏颊,它其實就是 NSTimer
,當(dāng)其加入到 RunLoop 時艾帐,RunLoop會注冊對應(yīng)的時間點乌叶,當(dāng)時間點到時,RunLoop會被喚醒以執(zhí)行那個回調(diào)柒爸。
Observer
Observer
對應(yīng)的類是 CFRunLoopObserverRef
准浴,當(dāng) RunLoop 的狀態(tài)發(fā)生變化時,觀察者就能通過回調(diào)接受到這個變化捎稚±趾幔可以觀測的時間點有以下幾個:
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
};
Runloop 的內(nèi)部邏輯
打開開頭的 Runloop
的源碼,面對眾多代碼今野,讓人毫無頭緒葡公,但是前文中已經(jīng)講到,屏幕的觸摸事件是 Runloop
來處理的条霜。于是打個斷點催什,來查看程序的函數(shù)調(diào)用棧:
從圖中能看到,Runloop
是從 11
開始的宰睡,于是從源碼中搜索 CFRunLoopRunSpecific
函數(shù)蒲凶,這里只探究內(nèi)部主要邏輯气筋,其他細(xì)節(jié)不看,下面是精簡后的函數(shù):
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
// 根據(jù) modeName 獲取currentMode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
// 設(shè)置 Runloop 的 Model
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
// 通知 Observers: 即將進(jìn)入 RunLoop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 進(jìn)入 runloop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知 Observers: RunLoop 即將退出
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
然后再進(jìn)入 __CFRunLoopRun(...)
函數(shù)查看內(nèi)部精簡后的主要邏輯源碼:
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
// 通知 Observers: 即將處理 Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知 Observers: 即將處理 Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 處理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 處理 Sources0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
// 處理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
// 判斷有無 Sources1
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
// 跳轉(zhuǎn)到 handle_msg 處理 Sources1soso
goto handle_msg;
}
// 通知 Observers: 即將休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 開始休眠
__CFRunLoopSetSleeping(rl);
// 等待消息喚醒當(dāng)前線程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
// 結(jié)束休眠
__CFRunLoopUnsetSleeping(rl);
// 通知 Observers: 結(jié)束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 處理
handle_msg:;
// 被 timer 喚醒
if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
// 處理 timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
}
// 被 gcd 喚醒
else if (livePort == dispatchPort) {
// 處理 gcd
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
// 被source1喚醒
} else {
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
}
// 處理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 設(shè)置返回值
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}
可以看到 Runloop
內(nèi)部確實是一個循環(huán)豹爹,并且裆悄,喚醒 RunLoop
的方式有 mach port
、Timer
和 dispatch
臂聋。筆者最初在疑惑一個問題光稼,上面的函數(shù)調(diào)用棧是一個點擊屏幕后的響應(yīng)事件,可以看出這里是 sources0
孩等,明明是一個觸摸事件為什么不是 sources1
呢艾君,筆者猜測 sources1
這里喚醒了 Runloop
,因為 sources0
是無法喚醒 runloop
的肄方,然后再在 sources0
的回調(diào)中處理的點擊事件冰垄。
RunLoop 中的 mach port
這里由于目前筆者水平有限,只能夠理解到 mach port
是一個可以控制硬件和接受硬件反饋的一個系統(tǒng)权她,然后可以通過它將來自硬件的操作轉(zhuǎn)化成熟知的 UIEvent
事件等等虹茶。
總結(jié)
這篇文章主要講解了 Runloop
到底是一個什么東西,當(dāng)然 Runloop
的知識不僅僅只有這篇文章這點隅要。例如實際用處中的線程焙铮活(AFNetworking 2.x 版本中),滑動時 Timer
怎么不被停止步清,自動釋放池的實現(xiàn)等等都用到了 Runloop
要门。