RunLoop

RunLoop 文章已經(jīng)很多了,結(jié)合各大文章做個總結(jié)

什么是 RunLoop

RunLoop 人如其名骡显,run 跑星爪,loop 循環(huán)浆西,無限的跑,類似于程序中的無限循環(huán)顽腾,RunLoop 是與線程相關(guān)聯(lián)的基礎(chǔ)架構(gòu)的一部分近零。 RunLoop 是一個事件處理循環(huán),用于調(diào)度和協(xié)調(diào)接收到的事件抄肖。 RunLoop 的目的是保持你的線程活著久信,在有消息到來的時候喚醒,沒有消息的時候休眠漓摩。
類似的機(jī)制在其他系統(tǒng)也存在
如在 Windows 中的 Message Loop

while(GetMessage(&Msg, NULL, 0, 0) > 0) 
{ 
    TranslateMessage(&Msg); 
    DispatchMessage(&Msg); 
} 

在安卓中的 MessageQueue 和 Looper

public class Looper {
    public static final void loop() {
        Looper me = myLooper();
        MessageQueue queue = me.mQueue;
        while (true) {
            Message msg = queue.next(); 
            if (msg != null) {
                if (msg.target == null) {
                    return;
                }
                msg.target.dispatchMessage(msg);
                msg.recycle();
            }
        }
    }
}

為什么需要 RunLoop

一般來說一個線程一次只能執(zhí)行一個任務(wù)裙士,執(zhí)行完成后線程就會退出。對 APP 來說管毙,這顯然是不符合我們的要求的腿椎,沒有人希望 APP 的應(yīng)用在啟動之后就自動退出的。這時候就需要一個機(jī)制讓線程能隨時處理事件但并不退出锅风,如主線程酥诽,在我們啟動之后可以一直存在,當(dāng)交互發(fā)生皱埠,又可以去處理相關(guān)的事件肮帐。傳統(tǒng)的死循環(huán)似乎是可以滿足我們的要求,但是死循環(huán)會導(dǎo)致 CPU 一直空轉(zhuǎn)大量消耗系統(tǒng)性能,RunLoop 真是為了這種場景情況下而生训枢,在沒有收到消息的時候會讓線程睡眠以避免資源占用托修、在有消息到來時立刻被喚醒。

NSRunLoop 和 CFRunLoopRef

NSRunLoop 基于 CFRunLoopRef 封裝恒界。
CFRunLoopRef 是在 CoreFoundation 框架內(nèi)的睦刃,它提供了純 C 函數(shù)的 API,所有這些 API 都是線程安全的十酣。
NSRunLoop 提供了面向?qū)ο蟮?API涩拙,這些 API 不是線程安全的。

RunLoop 執(zhí)行的流程

這邊用 ibireme 博客的圖片和代碼來說明 RunLoop 執(zhí)行的過程


內(nèi)部代碼

/// 用DefaultMode啟動
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
 
/// 用指定的Mode啟動耸采,允許設(shè)置RunLoop超時時間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
 
/// RunLoop的實現(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)旭贬,直接處理這個 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)入休眠, 直到被下面某一個事件喚醒。
            /// ? 一個基于 port 的Source 的事件岸军。
            /// ? 一個 Timer 到時間了
            /// ? RunLoop 自身的超時時間到了
            /// ? 被其他什么調(diào)用者手動喚醒
            __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 如果一個 Timer 到時間了,觸發(fā)這個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 如果一個 Source1 (基于port) 發(fā)出事件了须喂,處理這個事件
            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ù)說處理完事件就返回吁断。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出傳入?yún)?shù)標(biāo)記的超時時間了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部調(diào)用者強(qiáng)制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                /// source/timer/observer一個都沒有了
                retVal = kCFRunLoopRunFinished;
            }
            
            /// 如果沒超時,mode里沒空坞生,loop也沒被停止仔役,那繼續(xù)loop。
        } while (retVal == 0);
    }
    
    /// 10. 通知 Observers: RunLoop 即將退出是己。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

RunLoop 之 mode

熟悉 NSTimer 的同學(xué)一定知道當(dāng)使用 scheduledTimerWithTimeInterval 系列 API 的時候在 UIScrollView 以及其子類滾動的時候是不會調(diào)用回調(diào)又兵,如果我們需要要在滾動過程中也可以調(diào)用回調(diào)回調(diào)那就應(yīng)該這么寫

NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(repeat:) userInfo:nil repeats:true];
[[NSRunLoop currentRunLoop] addTimer:timer forMode: NSRunLoopCommonModes];

為什么這邊涉及到了 RunLoop ?這里的 NSRunLoopCommonModes 又是什么玩意?

這是于 RunLoop 運(yùn)行的機(jī)制有關(guān)沛厨,一個 RunLoop 的結(jié)構(gòu)大概如下


struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

可以看出宙地,在 RunLoop 執(zhí)行的時候總是會處在某一個 mode 狀態(tài)下。如剛進(jìn)入 APP 的時候啥事都沒干逆皮,這個時候 RunLoop 運(yùn)行的 mode 是 NSDefaultRunLoopMode(kCFRunLoopDefaultMode)宅粥,如果頁面有個 tableView 你滑動之后這個時候 RunLoop 會切換到 UITrackingRunLoopMode 模式下。NSTimer 的 scheduledTimerWithTimeInterval 系列的 API 默認(rèn)是將 timer 添加到 NSDefaultRunLoopMode 下的电谣,所以秽梅,當(dāng)你滑動 ScrollView 的時候切換了 mode ,當(dāng)然不能正常會調(diào)你的方法了剿牺。

RunLoop mode 的種類

RunLoop 系統(tǒng)預(yù)置的 mode 大概有幾個常用的

  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默認(rèn)模式企垦。
  • UITrackingRunLoopMode:滑動模式,在滑動 ScrollView 的時候會在此模式牢贸。
  • NSRunLoopCommonModes:通用模式竹观,在滑動 ScrollView 和默認(rèn)狀態(tài)都會響應(yīng)事件。

CommonModes

一個 Mode 可以將自己設(shè)置為"CommonMode"(通過將其 ModeName 添加到 RunLoop 的 "commonModes" 中)潜索。每當(dāng) RunLoop 的內(nèi)容發(fā)生變化時臭增,RunLoop 都會將事件同步到 CommonMode 的 mode item,什么是 mode item 后文會講到竹习。這也解釋了為什么 timer 加入到 NSRunLoopCommonModes 中會被正確的回調(diào)誊抛。

管理mode

在 NSRunLoop 這個層面不提供操作 mode 的 API ,在 CFRunLoopRef 提供相關(guān)的管理 API,不過只提供了增加 mode 的方法,不提供刪除的方法整陌。

// 添加一個  CommonMode
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
//是否在 某個 mode 下
CFRunLoopRunInMode(CFStringRef modeName, ...);

RunLoop 之 mode item

上文的 RunLoop 流程圖中我們可以看到 Source0拗窃, Source1,Timer泌辫,Observer 随夸,這些都是 mode item。
上一小節(jié)震放,介紹了 RunLoop mode宾毒,我們可以看看 mode 的結(jié)構(gòu)

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};

結(jié)合圖片理解


可以看出,一個 mode 由 _sources0殿遂,_sources1诈铛,_observers,_timers構(gòu)成墨礁。在也是每個 RunLoop 周期醒來之后要干的活幢竹。

  • Source0 只包含了一個回調(diào)(函數(shù)指針),它并不能主動觸發(fā)事件恩静。使用時焕毫,你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個 Source 標(biāo)記為待處理,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop咬荷,讓其處理這個事件冠句。

  • Source1 包含了一個 mach_port 和一個回調(diào)(函數(shù)指針),被用于通過內(nèi)核和其他線程相互發(fā)送消息幸乒。這種 Source 能主動喚醒 RunLoop 的線程懦底。

  • timer 是基于時間的觸發(fā)器,它和 NSTimer 是toll-free bridged 的罕扎,可以混用聚唐。其包含一個時間長度和一個回調(diào)(函數(shù)指針)。當(dāng)其加入到 RunLoop 時腔召,RunLoop會注冊對應(yīng)的時間點(diǎn)杆查,當(dāng)時間點(diǎn)到時,RunLoop會被喚醒以執(zhí)行那個回調(diào)臀蛛。

  • 是觀察者亲桦,每個 Observer 都包含了一個回調(diào)(函數(shù)指針),當(dāng) RunLoop 的狀態(tài)發(fā)生變化時浊仆,觀察者就能通過回調(diào)接受到這個變化客峭。可以觀測的時間點(diǎn)有以下幾個:

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
};

一個 item 可以被同時加入多個 mode抡柿。但一個 item 被重復(fù)加入同一個 mode 時是不會有效果的舔琅。如果一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出洲劣,不進(jìn)入循環(huán)备蚓。

更詳細(xì)的內(nèi)容可以查看博客:http://blog.ibireme.com/2015/05/18/runloop/

RunLoop 啟動和退出

啟動

這邊以 NSRunLoop 為例,CFRunLoopRef 類似囱稽,
啟動有3個方法

  • run
  • runUntilDate:
  • runMode:beforeDate:

run 底層是不斷(循環(huán))調(diào)用runMode:beforeDate:來達(dá)到運(yùn)行目的郊尝。
runUntilDate:底層也是調(diào)用runMode:beforeDate:來運(yùn)行,和run不同的是战惊,在指定的時間也就是 UntilDate 參數(shù)后會停止調(diào)用虚循。

退出

在系統(tǒng)提供的停止 RunLoop 方法只有 CFRunLoopStop()CFRunLoopStop() 方法只會結(jié)束當(dāng)前的 RunLoop 調(diào)用样傍,而不會結(jié)束后續(xù)的調(diào)用。也就意味著 如果你是用方法一也就是 run的方式啟動 RunLoop铺遂,那么這個 RunLoop 不會被退出衫哥,因為它會不斷的啟動,因為run 底層是不斷(循環(huán))調(diào)用runMode:beforeDate:來達(dá)到運(yùn)行目的襟锐。如果你是使用runUntilDate:啟動的撤逢,那么超時結(jié)束后會自動終止 RunLoop,如果是runMode:beforeDate:那么你可以精確的控制 RunLoop 的停止。

RunLoop 應(yīng)用

RunLoop 用途廣泛蚊荣,如 AutoreleasePool初狰,PerformSelecter,GCD互例,AsyncDisplayKit等都有涉及到奢入,更多神秘等著我們?nèi)ヌ剿鳌?/p>

后話

根據(jù) RunLoop 我們可以完成一個主線程卡頓的監(jiān)控工具,在計劃中媳叨,完成后會貼出地址腥光。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市糊秆,隨后出現(xiàn)的幾起案子武福,更是在濱河造成了極大的恐慌,老刑警劉巖痘番,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捉片,死亡現(xiàn)場離奇詭異,居然都是意外死亡汞舱,警方通過查閱死者的電腦和手機(jī)伍纫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來兵拢,“玉大人翻斟,你說我怎么就攤上這事∷盗澹” “怎么了访惜?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長腻扇。 經(jīng)常有香客問我债热,道長,這世上最難降的妖魔是什么幼苛? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任窒篱,我火速辦了婚禮,結(jié)果婚禮上舶沿,老公的妹妹穿的比我還像新娘墙杯。我一直安慰自己,他們只是感情好括荡,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布高镐。 她就那樣靜靜地躺著,像睡著了一般畸冲。 火紅的嫁衣襯著肌膚如雪嫉髓。 梳的紋絲不亂的頭發(fā)上观腊,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天,我揣著相機(jī)與錄音算行,去河邊找鬼梧油。 笑死,一個胖子當(dāng)著我的面吹牛州邢,可吹牛的內(nèi)容都是我干的儡陨。 我是一名探鬼主播,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼偷霉,長吁一口氣:“原來是場噩夢啊……” “哼迄委!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起类少,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤叙身,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后硫狞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體信轿,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年残吩,在試婚紗的時候發(fā)現(xiàn)自己被綠了财忽。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡泣侮,死狀恐怖即彪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情活尊,我是刑警寧澤隶校,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站蛹锰,受9級特大地震影響深胳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜铜犬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一舞终、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧癣猾,春花似錦敛劝、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至呐粘,卻和暖如春满俗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背作岖。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工唆垃, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人痘儡。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓辕万,卻偏偏與公主長得像,于是被迫代替她去往敵國和親沉删。 傳聞我的和親對象是個殘疾皇子渐尿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評論 2 349

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