強烈推薦 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)用為止。例如上面的搬磚事件蛮粮,吃飯事件益缎。處理完后,又會進入睡覺狀態(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 進入瘤袖。主線程的 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), // 即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
};
Runloop 的內(nèi)部邏輯
打開開頭的 Runloop 的源碼裳擎,面對眾多代碼,讓人毫無頭緒思币,但是前文中已經(jīng)講到鹿响,屏幕的觸摸事件是 Runloop 來處理的。于是打個斷點谷饿,來查看程序的函數(shù)調(diào)用棧:
image
從圖中能看到惶我,Runloop 是從 11 開始的,于是從源碼中搜索 CFRunLoopRunSpecific 函數(shù)博投,這里只探究內(nèi)部主要邏輯绸贡,其他細節(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: 即將進入 RunLoop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 進入 runloop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知 Observers: RunLoop 即將退出
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
然后再進入 __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 惰爬。