iOS-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)用為止。例如上面的搬磚事件蛮粮,吃飯事件益缎。處理完后,又會進入睡覺狀態(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 進入瘤袖。主線程的 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

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 惰爬。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市惫企,隨后出現(xiàn)的幾起案子撕瞧,更是在濱河造成了極大的恐慌,老刑警劉巖狞尔,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丛版,死亡現(xiàn)場離奇詭異,居然都是意外死亡偏序,警方通過查閱死者的電腦和手機页畦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來研儒,“玉大人豫缨,你說我怎么就攤上這事《硕洌” “怎么了好芭?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長冲呢。 經(jīng)常有香客問我舍败,道長,這世上最難降的妖魔是什么敬拓? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任邻薯,我火速辦了婚禮,結(jié)果婚禮上乘凸,老公的妹妹穿的比我還像新娘厕诡。我一直安慰自己,他們只是感情好翰意,可當(dāng)我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布木人。 她就那樣靜靜地躺著,像睡著了一般冀偶。 火紅的嫁衣襯著肌膚如雪醒第。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天进鸠,我揣著相機與錄音稠曼,去河邊找鬼。 笑死客年,一個胖子當(dāng)著我的面吹牛霞幅,可吹牛的內(nèi)容都是我干的漠吻。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼司恳,長吁一口氣:“原來是場噩夢啊……” “哼途乃!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起扔傅,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤耍共,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后猎塞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體试读,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年荠耽,在試婚紗的時候發(fā)現(xiàn)自己被綠了钩骇。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡铝量,死狀恐怖倘屹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情慢叨,我是刑警寧澤唐瀑,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站插爹,受9級特大地震影響哄辣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜赠尾,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一力穗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧气嫁,春花似錦当窗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至梯影,卻和暖如春巫员,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背甲棍。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工简识, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓七扰,卻偏偏與公主長得像奢赂,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子颈走,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,573評論 2 353

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