iOS 透過CFRunloop源碼分析runloop底層原理及應(yīng)用場景

一、runloop是什么?

從字面是理解就是循環(huán),無限循環(huán),app應(yīng)用從啟動(dòng)到退出,這個(gè)循環(huán)一直存在,啟動(dòng)就會(huì)自動(dòng)創(chuàng)建一個(gè)runloop,這也是app保持運(yùn)行不退出的關(guān)鍵所在。無限循環(huán)可以從CFRunloop的源碼可以看出:

?do{

///....

??}while(0== retVal);

二、runloop在干什么?

我們看下runloop的定義,在源碼中,runloop是一個(gè)結(jié)構(gòu)體,而結(jié)構(gòu)體中有一個(gè)重要的成員CFRunloopModelRef:

CFRunloop結(jié)構(gòu)體

我們再看看CFRunloopModelRef的數(shù)據(jù)結(jié)構(gòu):

CFRunloopModelRef數(shù)據(jù)結(jié)構(gòu)

看到CFRunloopModelRef的結(jié)構(gòu)體的成員中有,source0,source1,observers,timer,port等成員。這個(gè)跟runloop是什么樣的關(guān)系呢?我們看看runloop的運(yùn)行原理了,蘋果官方有張圖:

runloop運(yùn)行原理

其實(shí)NSRunLoop的本質(zhì)是一個(gè)消息機(jī)制的處理模式,runloop的運(yùn)行,其實(shí)就是不停的通過observer監(jiān)聽各種事件,包含各種source事件,timer,port等等,如果有這些事件,那就處理,沒有事件,就會(huì)進(jìn)入休眠,不停的重復(fù)上述過程。由此形成了運(yùn)行->檢測->休眠 ->運(yùn)行 的循環(huán)狀態(tài)。我們注意到source事件,timer,port,observer這些都是放在mode中,所以還有下面一種圖,才能完整描述runloop:

runloop

對于mode的類型,iOS系統(tǒng)給出了5種,分別是:

、、、

NSDefaultRunLoopMode ? ?// App默認(rèn)Mode通常主線程是在這個(gè)mode下運(yùn)行

UITrackingRunloopMode ? ?//界面跟蹤Mode用于scrollView追蹤觸摸界面滑動(dòng)時(shí)不受其他Mode影響

UIinitializationRunloopMode ? ?//在app一啟動(dòng)進(jìn)入的第一個(gè)Mode,啟動(dòng)完成后就不再使用

GSEventRecieveRunloopMode //蘋果使用繪圖相關(guān),系統(tǒng)內(nèi)核調(diào)用,開發(fā)者使用不到

NSRunLoopCommonModes   //占位模式

、、、

開發(fā)者能使經(jīng)常使用的就三種模式(NSDefaultRunLoopMode、UITrackingRunloopMode、NSRunLoopCommonModes),runloop會(huì)根據(jù)事件在不同模式之間自動(dòng)切換,例如當(dāng)scrollView的滑動(dòng)事件,系統(tǒng)的主runloop會(huì)切換到UITrackingRunloopMode模式下,這是NSDefaultRunLoopMode中的事件就會(huì)停止,直到scrollView的滑動(dòng)停止,才會(huì)切換回NSDefaultRunLoopMode模式下。當(dāng)然還可自定義mode,并添加runloop中,這里就不深入講解了。

三、工作過程:

下圖是runloop的工作過程:

runloop工作過程

我們可以看下源碼的__CFRunLoopRun()函數(shù):

/// 用DefaultMode啟動(dòng)

void CFRunLoopRun(void) {

? ? CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);

}

/// 用指定的Mode啟動(dòng),允許設(shè)置RunLoop超時(shí)時(shí)間

int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {

? ? return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);

}

/// RunLoop的實(shí)現(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),直接處理這個(gè) 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)入休眠, 直到被下面某一個(gè)事件喚醒。

? ? ? ? ? ? /// ? 一個(gè)基于 port 的Source 的事件。

? ? ? ? ? ? /// ? 一個(gè) Timer 到時(shí)間了

? ? ? ? ? ? /// ? RunLoop 自身的超時(shí)時(shí)間到了

? ? ? ? ? ? /// ? 被其他什么調(diào)用者手動(dòng)喚醒

? ? ? ? ? ? __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 如果一個(gè) Timer 到時(shí)間了,觸發(fā)這個(gè)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 如果一個(gè) Source1 (基于port) 發(fā)出事件了,處理這個(gè)事件

? ? ? ? ? ? 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í)參數(shù)說處理完事件就返回。

? ? ? ? ? ? ? ? retVal = kCFRunLoopRunHandledSource;

? ? ? ? ? ? } else if (timeout) {

? ? ? ? ? ? ? ? /// 超出傳入?yún)?shù)標(biāo)記的超時(shí)時(shí)間了

? ? ? ? ? ? ? ? retVal = kCFRunLoopRunTimedOut;

? ? ? ? ? ? } else if (__CFRunLoopIsStopped(runloop)) {

? ? ? ? ? ? ? ? /// 被外部調(diào)用者強(qiáng)制停止了

? ? ? ? ? ? ? ? retVal = kCFRunLoopRunStopped;

? ? ? ? ? ? } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {

? ? ? ? ? ? ? ? /// source/timer/observer一個(gè)都沒有了

? ? ? ? ? ? ? ? retVal = kCFRunLoopRunFinished;

? ? ? ? ? ? }

? ? ? ? ? ? /// 如果沒超時(shí),mode里沒空,loop也沒被停止,那繼續(xù)loop。

? ? ? ? } while (retVal == 0);

? ? }

? ? /// 10. 通知 Observers: RunLoop 即將退出。

? ? __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

}

四、runloop與線程的關(guān)系:

runloop與線程是一一對應(yīng)的關(guān)系,我應(yīng)用運(yùn)行的主線程對應(yīng)著主runloop,上面講了默認(rèn)就是開啟的,否則我們的應(yīng)用就不能保持運(yùn)行;而對于后臺(tái)線程,它所對應(yīng)的runloop則是沒有開啟的,這也就是后臺(tái)線程執(zhí)行完任務(wù)就會(huì)自動(dòng)銷毀回收的,如果如果需要?jiǎng)t手動(dòng)開啟。

蘋果開發(fā)的接口中并沒有直接創(chuàng)建Runloop的接口,如果需要使用Runloop通常CFRunLoopGetMain()CFRunLoopGetCurrent()兩個(gè)方法來獲?。ㄍㄟ^上面的源代碼也可以看到,核心邏輯在_CFRunLoopGet_當(dāng)中),通過代碼并不難發(fā)現(xiàn)其實(shí)只有當(dāng)我們使用線程的方法主動(dòng)get?Runloop時(shí)才會(huì)在第一次創(chuàng)建該線程的Runloop,同時(shí)將它保存在全局的Dictionary中(線程和Runloop二者一一對應(yīng)),默認(rèn)情況下線程并不會(huì)創(chuàng)建Runloop(主線程的Runloop比較特殊,任何線程創(chuàng)建之前都會(huì)保證主線程已經(jīng)存在Runloop),同時(shí)在線程結(jié)束的時(shí)候也會(huì)銷毀對應(yīng)的Runloop。

五、runloop與autoReleasePool的關(guān)系:

AutoreleasePool是另一個(gè)與RunLoop相關(guān)討論較多的話題。其實(shí)從RunLoop源代碼分析,AutoreleasePool與RunLoop并沒有直接的關(guān)系,之所以將兩個(gè)話題放到一起討論最主要的原因是因?yàn)樵趇OS應(yīng)用啟動(dòng)后會(huì)注冊兩個(gè)Observer管理和維護(hù)AutoreleasePool。不妨在應(yīng)用程序剛剛啟動(dòng)時(shí)打印currentRunLoop可以看到系統(tǒng)默認(rèn)注冊了很多個(gè)Observer,其中有兩個(gè)Observer的callout都是**?_?wrapRunLoopWithAutoreleasePoolHandler**,這兩個(gè)是和自動(dòng)釋放池相關(guān)的兩個(gè)監(jiān)聽。

第一個(gè)Observer會(huì)監(jiān)聽RunLoop的進(jìn)入,它會(huì)回調(diào)objc_autoreleasePoolPush()向當(dāng)前的AutoreleasePoolPage增加一個(gè)哨兵對象標(biāo)志創(chuàng)建自動(dòng)釋放池。這個(gè)Observer的order是-2147483647優(yōu)先級最高,確保發(fā)生在所有回調(diào)操作之前。

第二個(gè)Observer會(huì)監(jiān)聽RunLoop的進(jìn)入休眠和即將退出RunLoop兩種狀態(tài),在即將進(jìn)入休眠時(shí)會(huì)調(diào)用objc_autoreleasePoolPop()?和?objc_autoreleasePoolPush()?根據(jù)情況從最新加入的對象一直往前清理直到遇到哨兵對象。而在即將退出RunLoop時(shí)會(huì)調(diào)用objc_autoreleasePoolPop()?釋放自動(dòng)自動(dòng)釋放池內(nèi)對象。這個(gè)Observer的order是2147483647,優(yōu)先級最低,確保發(fā)生在所有回調(diào)操作之后。

主線程的其他操作通常均在這個(gè)AutoreleasePool之內(nèi)(main函數(shù)中),以盡可能減少內(nèi)存維護(hù)操作(當(dāng)然你如果需要顯式釋放【例如循環(huán)】時(shí)可以自己創(chuàng)建AutoreleasePool否則一般不需要自己創(chuàng)建)。

在子線程你創(chuàng)建了 Pool 的話,產(chǎn)生的 Autorelease 對象就會(huì)交給 pool 去管理。如果你沒有創(chuàng)建 Pool ,但是產(chǎn)生了 Autorelease 對象,就會(huì)調(diào)用 autoreleaseNoPage 方法。在這個(gè)方法中,會(huì)自動(dòng)幫你創(chuàng)建一個(gè) hotpage(hotPage 可以理解為當(dāng)前正在使用的 AutoreleasePoolPage,如果你還是不理解,可以先看看 Autoreleasepool 的源代碼,再來看這個(gè)問題 ),并調(diào)用page->add(obj)將對象添加到 AutoreleasePoolPage 的棧中,也就是說你不進(jìn)行手動(dòng)的內(nèi)存管理,也不會(huì)內(nèi)存泄漏啦!我們看看autoreleaseNoPage的源碼:

static __attribute__((noinline))

id *autoreleaseNoPage(id obj)

{

? ? // No pool in place.

? ? // hotPage 可以理解為當(dāng)前正在使用的 AutoreleasePoolPage。

? ? assert(!hotPage());

? ?// POOL_SENTINEL 只是 nil 的別名

? ? if(obj != POOL_SENTINEL? &&? DebugMissingPools) {

? ? ? ? // We are pushing an object with no pool in place,

? ? ? ? // and no-pool debugging was requested by environment.

? ? ? ? _objc_inform("MISSING POOLS: Object %p of class %s "

?? ? ? ? ? ? ? ? ? ? "autoreleased with no pool in place - "

?? ? ? ? ? ? ? ? ? ? "just leaking - break on "

?? ? ? ? ? ? ? ? ? ? "objc_autoreleaseNoPool() to debug",

?? ? ? ? ? ? ? ? ? ? (void*)obj, object_getClassName(obj));

? ? ? ? objc_autoreleaseNoPool(obj);

? ? ? ? returnnil;

? ? }

? ??// Install the first page.

? ? // 幫你創(chuàng)建一個(gè) hotpage(hotPage 可以理解為當(dāng)前正在使用的 AutoreleasePoolPage

? ? AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);

? ? setHotPage(page);

??// Push an autorelease pool boundary if it wasn't already requested.

? ? // POOL_SENTINEL 只是 nil 的別名,哨兵對象

? ? if(obj != POOL_SENTINEL) {

? ? ? ? page->add(POOL_SENTINEL);

? ? }

? ??// Push the requested object.

? ? // 把對象添加到 自動(dòng)釋放池 進(jìn)行管理

? ? returnpage->add(obj);

}

六、runloop在開發(fā)中的應(yīng)用

1、線程常駐:

我們知道,讓一個(gè)后臺(tái)線程常駐,需要讓它對應(yīng)的runloop run起來,并且還要往里面添加一個(gè)timer或者一個(gè)port,不然線程執(zhí)行完任務(wù)就會(huì)立馬銷毀。

線程已經(jīng)銷毀

我們講線程對應(yīng)的runloop開啟,代碼執(zhí)行:

常駐線程

2、UITableView滑動(dòng)卡頓優(yōu)化

UITableView滑動(dòng)卡頓的原因一般主要有以下幾種:

1、每次重新計(jì)算cell布局和高度,導(dǎo)致計(jì)算量大;

2、cell上加載多張超大高清圖片,導(dǎo)致計(jì)算量大,渲染超時(shí);

3、動(dòng)態(tài)創(chuàng)建添加子視圖;

4、過多使用透明視圖,導(dǎo)致離屏渲染。

網(wǎng)上可以找到很多對應(yīng)的解決方案,這里就不多描述了,主要講下第2點(diǎn)的解決方案(runloop方案),思路如下:

tableView加載過多的高清大圖,Runloop不只處理iOS事件,渲染圖形也是runloop處理的。

? ? ? 而渲染圖形的UI操作必須在主線程中,不能開辟線程進(jìn)行圖形處理。

? ? ? 在拖動(dòng)tableView的時(shí)候,Runloop要處理拖動(dòng)事件,還要處理過多圖片渲染,而造成卡頓。

解決卡頓分析:

? ? ? 1、Runloop在一次循環(huán)渲染圖片過多,那就減少Runloop一次處理圖片的數(shù)量,最多一次三張;

? ? ? 2、將處理圖片的代碼放在block中,然后加入數(shù)組中,處理幾次加入幾次。

? ? ? 3、我們只需要渲染,tableView顯示的圖片,顯示圖片有最大個(gè)數(shù)。移開屏幕或者不處理的從隊(duì)列數(shù)組里刪去;

? ? ? 4、滑動(dòng)的時(shí)候,不處理渲染任務(wù);

代碼如下,具體見demo

///保持runloop一直運(yùn)轉(zhuǎn)

-(NSTimer*)taskTimer{

? ? if(!_taskTimer) {

? ? ? ? _taskTimer = [NSTimer scheduledTimerWithTimeInterval:0.001 repeats:YES block:^(NSTimer * _Nonnull timer) {

? ? ? ? }];

? ? }

? ? return _taskTimer;

}

///添加到任務(wù)隊(duì)列中

-(void)addTask:(RunloopTask)task{

? ? [self.tasksaddObject:task];

? ? if(self.tasks.count>MAXTASKS) {

? ? ? ? [self.tasks removeObjectAtIndex:0];

? ? }

}

#pragma mark 向runloop中注冊observer

- (void)addObserver{

? ? //拿到當(dāng)前的Runloop

? ? CFRunLoopRef runloop = CFRunLoopGetCurrent();

? ? CFRunLoopObserverContext context = {

? ? ? ? 0,

? ? ? ? (__bridgevoid*)(self),

? ? ? ? &CFRetain,

? ? ? ? &CFRelease,

? ? ? ? NULL

? ? };

? ??//定義一個(gè)觀察者,static內(nèi)存中只存在一個(gè)

? ? static CFRunLoopObserverRef obverser;

? ? //創(chuàng)建一個(gè)觀察者

? ? obverser =CFRunLoopObserverCreate(NULL, kCFRunLoopAfterWaiting, YES, 0, &callBack, &context);

? ? //添加觀察者?。。∧J(rèn)模式下,滑動(dòng)的時(shí)候不處理渲染任務(wù)

? ? CFRunLoopAddObserver(runloop, obverser, kCFRunLoopDefaultMode);

? ??//release

? ? CFRelease(obverser);

}

///runloop即將休眠的observer回調(diào)

void?callBack (CFRunLoopObserverRef observer,CFRunLoopActivity activity,void*info){

? ? HXWRunloopTask*runloopTask = (__bridgeHXWRunloopTask*)info;

? ? if(runloopTask.tasks.count==0){

? ? ? ? return;

? ? }

?? ?///從任務(wù)隊(duì)列中去任務(wù)執(zhí)行

? ? RunloopTask task = runloopTask.tasks.firstObject;

? ? task();

? ? [runloopTask.tasks removeObjectAtIndex:0];

}

runloop應(yīng)用很廣泛,比如還有NSTimer的,需要添加到runloop中才能有效,GCD的異步提交到主隊(duì)列,上面提到過的自動(dòng)釋放池等等,在這里就不一一敘述了。

以上,歡迎各位指正。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容