RunLoop淺析

什么是Runloop

RunLoop01.png
  • 運(yùn)行循環(huán)
  • 跑圈
  • 內(nèi)部類似一個 do-while 循環(huán), 在循環(huán)內(nèi)部不斷處理各種任務(wù) (Source, Observe, Timer)
  • 一個線程對應(yīng)一個 RunLoop

用途

  • 保持程序持續(xù)運(yùn)行
  • 處理 APP 各種事件 (觸摸事件, 定時器事件, Selector事件)
  • 節(jié)省 CPU 資源, 提高程序性能: 該做事情的時候做事情, 該休息時休息

沒有RunLoop

程序一啟動就結(jié)束了

int main(int argc, char * argv[]) {
    NSLog(@"execute main function");
    return 0;
}

如果有了 RunLoop

程序大致是這樣子,但是要更加復(fù)雜

int main(int argc, char * argv[]) {
    BOOL running = YES;
    do {
        // 執(zhí)行各種任務(wù),處理各種事件
             // ......
    } while (running);
    return 0;
}

由于 main 函數(shù)里面啟動了一個 RunLoop, 因此程序不會馬上退出, 會保持程序的運(yùn)行狀態(tài)

main 函數(shù)中的 RunLoop

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

  • UIApplicationMain 函數(shù)內(nèi)部啟動了一個 RunLoop 對象
  • UIApplicationMain 函數(shù)一直沒有返回, 保持了程序的運(yùn)行
  • 這個默認(rèn)啟動的 RunLoop 是與主線程相關(guān)

程序一旦啟動

  • 執(zhí)行UIApplicationMain 函數(shù)
  • 默認(rèn)啟動一個 RunLoop
  • 這個 RunLoop 會一直處理主線程相關(guān)的事情
  • 這個 RunLoop 會一直遍歷, 監(jiān)聽用戶事件
  • 這就是主線程的事件響應(yīng)的這么快的原因

RunLoop 要想跑圈

  • 模式(Mode)里面要有東西 (事件源 / Observer / 定時器)
  • RunLoop 要啟動 (主線程默認(rèn)創(chuàng)建并啟動, 子線程需要手動啟動)
  • 沒有事件源, 沒有定時器, RunLoop 就會進(jìn)入睡眠狀態(tài)

RunLoop 對象

iOS 中提供了兩套 API 來訪問和使用 RunLoop

  • Foundation : NSRunLoop
  • Core Foundation : CFRunLoopRef

NSRunLoop 是基于 CFRunLoopRef 的OC 包裝, 如果研究 RunLoop 內(nèi)部結(jié)構(gòu), 需要研究 CFRunLoopRef

RunLoop 與線程

  • 每條線程都有唯一一個與之對應(yīng)的 RunLoop 對象
  • 主線程的 RunLoop 已經(jīng)創(chuàng)建好, 子線程的 RunLoop 需要手動創(chuàng)建
  • RunLoop 在第一次獲取時創(chuàng)建, 在線程結(jié)束時銷毀
  • RunLoop 對象是使用字典存儲, 以線程作為 key

RunLoop 相關(guān)類

RunLoop02.png

01 - CFRunLoopModeRef

  • CFRunLoopModeRef 代表著RunLoop的運(yùn)行模式
  • 一個RunLoop包含若干個Mode,每個Mode又包含若干個 Source/Timer/Observer
  • 每次RunLoop啟動時, 都會指定其中一個Mode, 這個Mode被稱作CurrentMode
  • 如果需要切換 Mode, 只能退出 RunLoop, 再重新指定一個 Mode 進(jìn)入

系統(tǒng)默認(rèn)注冊了 5 個 Mode :

  • kCFRunLoopDefultMode : APP 的默認(rèn) Mode, 通常主線程是在這個 Mode下
  • UITrackingRunLoopMode :
    界面跟蹤 Mode, 用于 scrollView 跟蹤觸摸滑動, 保證界面不受其他 Mode 影響 (添加定時器不好使)
  • UIInitializationRunLoopMode :
    在剛啟動 APP 時進(jìn)入的第一個 Mode, 啟動完就不再使用
  • GSEventReceiveRunLoopMode :
    接收系統(tǒng)事件的內(nèi)部 Mode, 通常用不到
  • kCFRunLoopCommonModes :
    這是一個占位用的 Mode, 不是一個真正的 Mode (也就說 RunLoop 無法啟動此模式)

02 - CFRunLoopTimerRef

  • CFRunLoopTimerRef 是基于時間的觸發(fā)器
  • 基本上相當(dāng)于 NSTimer
  • 定時器會跑在 common modes 模式下
  • 標(biāo)記為 common modes 的模式有:
    • kCFRunLoopDefultMode
    • UITrackingRunLoopMode

定時器添加到 kCFRunLoopDefultMode

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

將定時器添加到 NSDefaultRunLoopMode , 滑動 scollView 的時候, 定時器就會停止執(zhí)行, RunLoop 此時會自動切換到 UITrackingRunLoopMode 模式, 定時器就會停止執(zhí)行

定時器添加到 NSRunLoopCommonModes

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

將定時器添加到 NSRunLoopCommonModes, 此時就不會停止執(zhí)行

03 - CFRunLoopSourceRef

  • CFRunLoopSourceRef是事件源(輸入源)

以前的分法

  • Port-Based Sources
  • Custom Input Sources
  • Cocoa Perform Selector Sources

現(xiàn)在的分法

  • Source0:非基于Port的
  • Source1:基于Port的

04 - CFRunLoopObserverRef

  • CFRunLoopObserverRef是觀察者臀突,能夠監(jiān)聽RunLoop的狀態(tài)改變
- (void)observer
{
    // 創(chuàng)建observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"----監(jiān)聽到RunLoop狀態(tài)發(fā)生改變---%zd", activity);
    });

    // 添加觀察者:監(jiān)聽RunLoop的狀態(tài)
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

    // 釋放Observer
    CFRelease(observer);
}
  • 可以監(jiān)聽的時間點有以下幾個
/* Run Loop Observer Activities */
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
    kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有事件
};

RunLoop 處理邏輯

下圖簡介了 RunLoop 處理過程, 一個線程的 RunLoop 在存在事件源 / 定時器的條件下, 會不斷的處理事件, 處理的事件包括

  • 處理基于 port 的 CFRunLoopSourceRef
  • 處理 customer 自定義事件源
  • 處理 selector 事件
  • 處理定時器執(zhí)行
RunLoop處理邏輯(官方示意圖).png

官方的圖解很清楚, RunLoop 在不停的跑圈, 跑圈的前提是滿足以下條件之一:

  • 輸入源 (事件源), 即 CFRunLoopSourceRef, 基于端口的輸入源 (port) 和 自定義輸入源 (custom), 當(dāng)然還包含 performSelector:onThread...
  • 擁有添加在 RunLoop 內(nèi)的定時器
RunLoop處理邏輯(網(wǎng)友整理).png

RunLoop 實際應(yīng)用

(1) 常駐線程

即讓子線程處于 "不消亡" 的狀態(tài), 一直在后臺處理某些頻發(fā)事件 / 等待其他線程發(fā)來消息

  • 在子線程監(jiān)控網(wǎng)絡(luò)狀態(tài)
  • 在子線程開啟一個定時器
  • 在子線程長期監(jiān)控其他行為
+ (void)networkRequestThreadEntryPoint:(id)__unused object { 
    @autoreleasepool { 
        [[NSThread currentThread] setName:@"AFNetworking"];        
         NSRunLoop *runLoop = [NSRunLoop currentRunLoop];       
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; 
    }
}

摘自 AFNetworking 源代碼, AFN這樣做的原理在于子線程下默認(rèn)不開啟 RunLoop, 需要手動開啟, 而 RunLoop 不斷跑圈需要滿足以下條件之一 :

  • RunLoop有事件源(輸入源), 包含基于端口 (port) 的事件源 / custom 事件源等
  • RunLoop存在定時器
    因此, AFN為RunLoop的default模式增加了一個NSMachPort端口(實際上也可以是其他端口),也就相當(dāng)于為RunLoop添加了事件源, 因此RunLoop可以不斷的跑圈, 保證線程的不死狀態(tài)
    順便提一下, AFN保持一個常駐線程的原因, 第一是因為子線程默認(rèn)不會開啟RunLoop, 它會像一個C語言程序一樣運(yùn)行完所有代碼后退出線程, 而網(wǎng)絡(luò)請求是異步的, 這就可能會出現(xiàn)通過網(wǎng)絡(luò)請求獲取到數(shù)據(jù)之后, 線程已經(jīng)退出, 無法執(zhí)行請求成功/失敗的代理方法, 因此AFN開啟了一個RunLoop, 保活了線程

(2) 控制定時器在特定模式下運(yùn)行

即可以將計時器 timer 添加到 kCFRunLoopDefultMode 下, 如果 RunLoop 切換到 UITrackingRunLoopMode (UIScrollView 滾動過程中), 那么定時器就會暫停執(zhí)行, 等到滾動結(jié)束, 定時器就會繼續(xù)執(zhí)行
也可以將定時器 timer 添加到 NSRunLoopCommonModes 下, 此時不管有無 scrollView 滑動, 都不會影響 timer 的執(zhí)行

(3) 控制某些事件在特定模式下執(zhí)行

即可以讓某個 selector 在某個線程 (key) 的 RunLoop 下的特定模式下執(zhí)行 (數(shù)組中包含 Mode)

通過以下的 API :

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);

(4) 添加 Observer 監(jiān)聽 RunLoop 狀態(tài), 可以監(jiān)聽點擊事件的處理 (在所有點擊事件之前做一些事情)

調(diào)用 C 語言函數(shù) CFRunLoopObserverCreateWithHandler () 創(chuàng)建 Observer, 監(jiān)聽某個 RunLoop 狀態(tài), 注意要手動釋放

關(guān)于自動釋放池與 RunLoop

Autorelease pool

在沒有手加Autorelease Pool的情況下,Autorelease對象是在當(dāng)前的runloop迭代結(jié)束時釋放的捐腿,而它能夠釋放的原因是系統(tǒng)在每個runloop迭代中都加入了自動釋放池Push和Pop

也就意味著, 在@autorelasepool 中的代碼, 默認(rèn)都是加在了一個自動釋放池當(dāng)中, 這個自動釋放池是與主線程的 RunLoop 相關(guān), 內(nèi)部所有對象會在自動釋放池釋放的時候?qū)?nèi)部所有對象進(jìn)行一次 release 操作

至于主線程 RunLoop 下的自動釋放池什么時候釋放, 是在主線程 RunLoop 迭代 (睡眠)之前釋放, 這個 RunLoop 什么時候睡眠呢? 是在沒有接收任何輸入源(事件源)/定時器的條件下

自動釋放池什么時候釋放?

在 RunLoop 睡眠之前釋放 (KCFRunLoopBeforeWaiting), 也有人說 Autorelease對象是在當(dāng)前的runloop迭代結(jié)束時釋放的, 實際是一個意思

什么時候用@autoreleasepool

根據(jù)Apple的文檔较店,使用場景如下:

  • 寫基于命令行的的程序時捞奕,就是沒有UI框架,如AppKit等Cocoa框架時桥狡。
  • 寫循環(huán)搅裙,循環(huán)里面包含了大量臨時創(chuàng)建的對象。(本文的例子)
  • 創(chuàng)建了新的線程总放。(非Cocoa程序創(chuàng)建線程時才需要)
  • 長時間在后臺運(yùn)行的任務(wù)呈宇。

RunLoop 研究資料

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市局雄,隨后出現(xiàn)的幾起案子甥啄,更是在濱河造成了極大的恐慌,老刑警劉巖炬搭,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蜈漓,死亡現(xiàn)場離奇詭異,居然都是意外死亡宫盔,警方通過查閱死者的電腦和手機(jī)融虽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來灼芭,“玉大人有额,你說我怎么就攤上這事。” “怎么了巍佑?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵茴迁,是天一觀的道長。 經(jīng)常有香客問我萤衰,道長堕义,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任脆栋,我火速辦了婚禮倦卖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘椿争。我一直安慰自己怕膛,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布秦踪。 她就那樣靜靜地躺著嘉竟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪洋侨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天倦蚪,我揣著相機(jī)與錄音希坚,去河邊找鬼。 笑死陵且,一個胖子當(dāng)著我的面吹牛裁僧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播慕购,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼聊疲,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了沪悲?” 一聲冷哼從身側(cè)響起获洲,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎殿如,沒想到半個月后贡珊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涉馁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年门岔,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片烤送。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡寒随,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情妻往,我是刑警寧澤互艾,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站蒲讯,受9級特大地震影響忘朝,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜判帮,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一局嘁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧晦墙,春花似錦悦昵、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至抗楔,卻和暖如春棋凳,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背连躏。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工剩岳, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人入热。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓拍棕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親勺良。 傳聞我的和親對象是個殘疾皇子绰播,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,472評論 2 348

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