RunLoop

概述

RunLoop提供了一種機制:線程沒有任務(wù)執(zhí)行時,進入休眠狀態(tài)走贪,讓出CPU資源浪讳;當(dāng)有任務(wù)需要執(zhí)行的時候缰盏,喚醒線程。
Android的Looper淹遵、Nodejs的Event Loop都是類似的原理口猜。

基礎(chǔ)用法

創(chuàng)建runloop

- (void)viewDidLoad {
    [super viewDidLoad];

    // 創(chuàng)建線程,并調(diào)用run1方法執(zhí)行任務(wù)
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
    // 開啟線程
    [self.thread start];    
}

- (void) run1
{
    // 這里寫任務(wù)
    NSLog(@"----run1-----");

    // 添加下邊兩句代碼透揣,就可以開啟RunLoop济炎,之后self.thread就變成了常駐線程,可隨時添加任務(wù)辐真,并交于RunLoop處理
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];

    // 測試是否開啟了RunLoop须尚,如果開啟RunLoop崖堤,則來不了這里,因為RunLoop開啟了循環(huán)耐床。
    NSLog(@"未開啟RunLoop");
}

原理

組成部分

和runloop有關(guān)的幾個名詞:Thread密幔、CFRunLoopRef、CFRunLoopModeRef撩轰、CFRunLoopSourceRef胯甩、CFRunLoopTimerRef、CFRunLoopObserverRef

他們的關(guān)系是如下圖所示


runloop的組成
runloop的組成

1.Thread和runloop是一一對應(yīng)的钧敞。iOS主線程中默認創(chuàng)建了Runloop蜡豹,使主線程一直處于運行或休眠狀態(tài),沒有被系統(tǒng)銷毀溉苛。子線程默認是沒有runloop的镜廉,所以子線程執(zhí)行完任務(wù)后,會被系統(tǒng)銷毀愚战〗课ǎ可以手動在子線程中創(chuàng)建runloop,使子線程處于奔帕幔活狀態(tài)塔插。詳見基礎(chǔ)用法部分。

2.CFRunLoopRef代表runloop對象拓哟。創(chuàng)建runloop和runloop的方法詳見基礎(chǔ)用法部分想许。

3.CFRunLoopModeRef是runloop的運行模式。Runloop可以包含若干個Mode断序,每個Mode又包含Source/Timer/Observer流纹。當(dāng)切換Mode時必須退出當(dāng)前Mode,然后重新進入Runloop以保證不同Mode的Source/Timer/Observer互不影響违诗。 系統(tǒng)系統(tǒng)的Mode有:NSDefaultRunLoopMode漱凝、UITrackingRunLoopMode、UIInitializationRunLoopMode诸迟、GSEventReceiveRunLoopMode kCFRunLoopCommonModes茸炒。其中UIInitializationRunLoopMode是剛啟動App時第進入的第一個 Mode,啟動完成后就不再使用阵苇。NSDefaultRunLoopMode是默認的Mode壁公,UITrackingRunLoopMode是跟蹤用戶交互的Mode。kCFRunLoopCommonModes不是一種具體的Mode绅项,是而是一種模式組合贮尖,在iOS系統(tǒng)中默認包含了NSDefaultRunLoopMode和 UITrackingRunLoopMode。

4.CFRunLoopSourceRef是RunLoop的事件源趁怔。蘋果文檔將RunLoop能夠處理的事件分為Input sources和timer事件湿硝。


RunLoop事件源

Input sources分為source0和source1兩大類。其中source0主要處理應(yīng)用層的事件(不是內(nèi)核或其他進程發(fā)過來的)润努,如:performSelectors关斜、dispatch_async。Source1是基于mach_Port的,處理來自系統(tǒng)內(nèi)核或者其他進程或線程的事件铺浇,可以主動喚醒休眠中的RunLoop痢畜。mach_port大家就理解成進程間相互發(fā)送消息的一種機制就好, 比如屏幕點擊, 網(wǎng)絡(luò)數(shù)據(jù)的傳輸都會觸發(fā)sourse1。
簡單舉個例子:一個APP在前臺靜止著鳍侣,此時丁稀,用戶用手指點擊了一下APP界面,那么過程就是下面這樣的:
我們觸摸屏幕,先摸到硬件(屏幕)倚聚,屏幕表面的事件會被IOKit先包裝成Event,通過mach_Port傳給正在活躍的APP , Event先告訴source1(mach_port),source1喚醒RunLoop, 然后將事件Event分發(fā)給source0,然后由source0來處理线衫。

5.CFRunLoopTimerRef 定時器。在主線程的RunLoop的NSDefaultRunLoopMode中添加一個NSTimer惑折,然后滑動列表授账,會發(fā)現(xiàn)定時器不工作。是因為滑動列表后惨驶,主線程的Mode切換到UITrackingRunLoopMode白热,而定時器在NSDefaultRunLoopMode,所以定時器不工作粗卜∥萑罚可以把NSTimer添加到UITrackingRunLoopMode或者kCFRunLoopCommonModes中。

6.CFRunLoopObserverRef RunLoop狀態(tài)的監(jiān)聽者续扔」ネ危可以監(jiān)聽

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry , // 進入 loop
    kCFRunLoopBeforeTimers , // 觸發(fā) Timer 回調(diào)
    kCFRunLoopBeforeSources , // 觸發(fā) Source0 回調(diào)
    kCFRunLoopBeforeWaiting , // 等待 mach_port 消息
    kCFRunLoopAfterWaiting ), // 接收 mach_port 消息
    kCFRunLoopExit , // 退出 loop
    kCFRunLoopAllActivities  // loop 所有狀態(tài)改變
}

監(jiān)聽的代碼


        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            switch (activity) {
                case kCFRunLoopEntry:
                {
                    NSLog(@"zizhong,進入runloop");
                }
                    break;
                case kCFRunLoopBeforeTimers:
                {
                    NSLog(@"zizhong,timers");
                }
                    break;
                case kCFRunLoopBeforeSources:
                {
                    NSLog(@"zizhong,sources");
                }
                    break;
                case kCFRunLoopBeforeWaiting:
                {
                    NSLog(@"zizhong,即將進入休眠");
                }
                    break;
                case kCFRunLoopAfterWaiting:
                {
                    NSLog(@"zizhong,喚醒");
                }
                    break;
                case kCFRunLoopExit:
                {
                    NSLog(@"zizhong,退出");
                }
                    break;
                default:
                    break;
            }
        });
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);

運行原理

image.png

runloop偽代碼

int32_t __CFRunLoopRun()
{
    // 通知即將進入runloop
    __CFRunLoopDoObservers(KCFRunLoopEntry);
    
    do
    {
        // 通知將要處理timer和source
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
        
        // 處理非延遲的主線程調(diào)用
        __CFRunLoopDoBlocks();
        // 處理Source0事件
        __CFRunLoopDoSource0();
        
        if (sourceHandledThisLoop) {
            __CFRunLoopDoBlocks();
         }
        /// 如果有 Source1 (基于port) 處于 ready 狀態(tài),直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息测砂。
        if (__Source0DidDispatchPortLastTime) {
            Boolean hasMsg = __CFRunLoopServiceMachPort();
            if (hasMsg) goto handle_msg;
        }
            
        /// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)茵烈。
        if (!sourceHandledThisLoop) {
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
        }
            
        // GCD dispatch main queue
        CheckIfExistMessagesInMainDispatchQueue();
        
        // 即將進入休眠
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
        
        // 等待內(nèi)核mach_msg事件
        mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
        
        // 等待。砌些。呜投。
        
        // 從等待中醒來
        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
        
        // 處理因timer的喚醒
        if (wakeUpPort == timerPort)
            __CFRunLoopDoTimers();
        
        // 處理異步方法喚醒,如dispatch_async
        else if (wakeUpPort == mainDispatchQueuePort)
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
            
        // 處理Source1
        else
            __CFRunLoopDoSource1();
        
        // 再次確保是否有同步的方法需要調(diào)用
        __CFRunLoopDoBlocks();
        
    } while (!stop && !timeout);
    
    // 通知即將退出runloop
    __CFRunLoopDoObservers(CFRunLoopExit);
}

高級用法

點擊事件響應(yīng)

(1)用戶觸發(fā)事件->(2)系統(tǒng)將事件轉(zhuǎn)交到對應(yīng)APP的事件隊列->(3)APP從消息隊列頭取出事件->(4)交由Main Window進行消息分發(fā)->(5)找到合適的Responder進行處理,如果沒找到存璃,則會沿著Responder chain返回到APP層仑荐,丟棄不響應(yīng)該事件

用戶觸發(fā)事件, IOKit.framework 生成一個 IOHIDEvent 事件并由 SpringBoard 接收纵东,SpringBoard會利用mach port粘招,產(chǎn)生source1,來喚醒目標(biāo)APP的com.apple.uikit.eventfetch-thread的RunLoop偎球。Eventfetch thread會將main runloop 中__handleEventQueue所對應(yīng)的source0設(shè)置為signalled == Yes狀態(tài)洒扎,同時喚醒main RunLoop辑甜。mainRunLoop則調(diào)用__handleEventQueue進行事件隊列處理。

dispatch_async(dispatch_get_main_queue(), block)

如當(dāng)調(diào)用了 dispatch_async(dispatch_get_main_queue(), block)時袍冷,主隊列會把該 block 放到對應(yīng)的線程(恰好是主線程)中磷醋,主線程的 RunLoop 會被喚醒,從消息中取得這個 block胡诗,回調(diào) CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 來執(zhí)行這個 block

線程钡讼撸活

子線程默認是完成任務(wù)后結(jié)束。當(dāng)要經(jīng)常使用子線程煌恢,每次開啟子線程比較耗性能骇陈。此時可以開啟子線程的 RunLoop,保持 RunLoop 運行瑰抵,則使子線程保持不死你雌。AFNetworking 基于 NSURLConnection 時正是這樣做的,希望在后臺線程能保持活著谍憔,從而能接收到 delegate 的回調(diào)匪蝙。

           /* 返回一個線程 */
+ (NSThread *)networkRequestThread {
        static NSThread *_networkRequestThread = nil;
        static dispatch_once_t oncePredicate;
        dispatch_once(&oncePredicate, ^{
            // 創(chuàng)建一個線程,并在該線程上執(zhí)行下一個方法
            _networkRequestThread = [[NSThread alloc] initWithTarget:self
                                                            selector:@selector(networkRequestThreadEntryPoint:)
                                                              object:nil];
            // 開啟線程
            [_networkRequestThread start];
        });
        return _networkRequestThread;
    }
/* 在新開的線程中執(zhí)行的第一個方法 */
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        // 獲取當(dāng)前線程對應(yīng)的 RunLoop
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        // 為 RunLoop 添加 source习贫,模式為 DefaultMode
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        // 開始運行 RunLoop
        [runLoop run];
        / /或者
       //[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:4]];
    }
}

監(jiān)聽卡頓

核心思路:主線程runloop中添加監(jiān)聽逛球,監(jiān)聽runloop狀態(tài)變化。如果runloop長期處于kCFRunLoopBeforeSources(處理source0)或者kCFRunLoopAfterWaiting(處理source1)苫昌,就說明出現(xiàn)了卡頓颤绕。

自定義線程間通信

自定義線程間通信
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市祟身,隨后出現(xiàn)的幾起案子奥务,更是在濱河造成了極大的恐慌,老刑警劉巖袜硫,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件氯葬,死亡現(xiàn)場離奇詭異,居然都是意外死亡婉陷,警方通過查閱死者的電腦和手機帚称,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來秽澳,“玉大人闯睹,你說我怎么就攤上這事〉I瘢” “怎么了楼吃?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我孩锡,道長酷宵,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任浮创,我火速辦了婚禮忧吟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘斩披。我一直安慰自己,他們只是感情好讹俊,可當(dāng)我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布垦沉。 她就那樣靜靜地躺著,像睡著了一般仍劈。 火紅的嫁衣襯著肌膚如雪厕倍。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天贩疙,我揣著相機與錄音讹弯,去河邊找鬼。 笑死这溅,一個胖子當(dāng)著我的面吹牛组民,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播悲靴,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼臭胜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了癞尚?” 一聲冷哼從身側(cè)響起耸三,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎浇揩,沒想到半個月后仪壮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡胳徽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年积锅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片膜廊。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡乏沸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出爪瓜,到底是詐尸還是另有隱情蹬跃,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站蝶缀,受9級特大地震影響丹喻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜翁都,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一碍论、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧柄慰,春花似錦鳍悠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至概行,卻和暖如春蠢挡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背凳忙。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工业踏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人涧卵。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓勤家,卻偏偏與公主長得像,于是被迫代替她去往敵國和親艺演。 傳聞我的和親對象是個殘疾皇子却紧,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,514評論 2 348

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