iOS 淺談 Runloop

RunLoop 是什么

強烈推薦 ibireme 大神的文章深入理解RunLoop

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)在能夠得出如下圖所示的理解:

image

一個 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

  • kCFRunLoopDefaultModeAPP 的普通狀態(tài)才顿,通常主線程是在這個Mode下運行,已被標(biāo)記為 Common尤蒿。

  • UITrackingRunLoopModeApp 追蹤觸摸 ScrollView 滑動時的狀態(tài)郑气,保證界面滑動時不受其他 Mode影響,已被標(biāo)記為 Common腰池。

注意 Runloop 的結(jié)構(gòu)中有一個 _commonModes 尾组。這里是因為一個 Mode 可以將自己標(biāo)記為 Common (通過將其 ModeName 添加到 RunLoopcommonModes 中 )忙芒,標(biāo)記為 CommonModel 都可以處理事件,可以理解為變相的實現(xiàn)了多個 Model 同時運行讳侨。同時系統(tǒng)也提供了一個操作 Common 標(biāo)記的字符串->kCFRunLoopCommonModes呵萨。如果我們想要上面兩種模式下都能處理事件,就可以使用這個字符串跨跨。

Model 中的 Item

Source/Timer/Observer 被統(tǒng)稱為 mode item潮峦,不同 ModelSource0/Source1/Timer/Observer 被分隔開來,互不影響勇婴,如果 Mode 里沒有任何Source0/Source1/Timer/Observer忱嘹,RunLoop 會立馬退出。

Source

Source 是事件產(chǎn)生的的地方耕渴,它對應(yīng)的類為 CFRunLoopSourceRef拘悦。Source 有兩個版本:Source0Source1

  • 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)用棧:

image

從圖中能看到,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 portTimerdispatch

臂聋。筆者最初在疑惑一個問題光稼,上面的函數(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 要门。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市廓啊,隨后出現(xiàn)的幾起案子欢搜,更是在濱河造成了極大的恐慌,老刑警劉巖谴轮,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件炒瘟,死亡現(xiàn)場離奇詭異,居然都是意外死亡第步,警方通過查閱死者的電腦和手機疮装,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來雌续,“玉大人斩个,你說我怎么就攤上這事胯杭⊙倍牛” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵做个,是天一觀的道長鸽心。 經(jīng)常有香客問我滚局,道長,這世上最難降的妖魔是什么顽频? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任藤肢,我火速辦了婚禮,結(jié)果婚禮上糯景,老公的妹妹穿的比我還像新娘嘁圈。我一直安慰自己,他們只是感情好蟀淮,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布最住。 她就那樣靜靜地躺著,像睡著了一般怠惶。 火紅的嫁衣襯著肌膚如雪涨缚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天策治,我揣著相機與錄音脓魏,去河邊找鬼。 笑死通惫,一個胖子當(dāng)著我的面吹牛茂翔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播讽膏,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼檩电,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了府树?” 一聲冷哼從身側(cè)響起俐末,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎奄侠,沒想到半個月后卓箫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡垄潮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年烹卒,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弯洗。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡旅急,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出牡整,到底是詐尸還是另有隱情藐吮,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站谣辞,受9級特大地震影響迫摔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜泥从,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一句占、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧躯嫉,春花似錦纱烘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至昼弟,卻和暖如春啤它,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背舱痘。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工变骡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人芭逝。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓塌碌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親旬盯。 傳聞我的和親對象是個殘疾皇子台妆,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 這幾天研究了一下iOS的Runloop,看了不少的文章胖翰,收獲不少接剩,但是疑問也挺多。所以我就試著去翻譯了并分析總結(jié)了...
    擰發(fā)條鳥xds閱讀 1,393評論 0 10
  • 1 Runloop機制原理 深入理解RunLoop http://www.cocoachina.com/ios/2...
    Kevin_Junbaozi閱讀 3,983評論 4 30
  • Runloop 是和線程緊密相關(guān)的一個基礎(chǔ)組件萨咳,是很多線程有關(guān)功能的幕后功臣懊缺。盡管在平常使用中幾乎不太會直接用到,...
    jackyshan閱讀 9,843評論 10 75
  • 寫在前面 由于文章比較長培他,簡書沒有目錄鹃两,讀起來不方便。建議看有目錄版RunLoop從源碼到應(yīng)用全面解析——帶目錄版...
    紙簡書生閱讀 3,606評論 1 16
  • 概述 RunLoop作為iOS中一個基礎(chǔ)組件和線程有著千絲萬縷的關(guān)系舀凛,同時也是很多常見技術(shù)的幕后功臣俊扳。盡管在平時多...
    陽明先生_x閱讀 1,092評論 0 17