RunLoop機制

一直想寫關(guān)于RunLoop的文章攻泼,但是發(fā)現(xiàn)要完全搞明白實在是太難了揖赴。RunLoop好多設(shè)計到了內(nèi)核⊙┪唬現(xiàn)在只能說一說RunLoop的表層了竭钝。
首先RunLoop是跟線程相關(guān)的”⑾矗可以理解為輔助線程執(zhí)行任務(wù)的工具香罐。
如果沒有RunLoop線程的執(zhí)行順序就是任務(wù)執(zhí)行完就銷毀了。如果有了RunLoop就可以調(diào)用內(nèi)核 可以讓線程掛起 休眠时肿。節(jié)省CPU資源庇茫,下次有任務(wù)的時候再喚醒,節(jié)省線程開辟的消耗螃成。如果你是單一的任務(wù) 那么你就不用關(guān)系RunLoop了旦签。但是如果你是重復 并且是間斷執(zhí)行的時候,就可以使用RunLoop了寸宏。
先說下大概的使用場景

RunLoop應(yīng)用

1.NSTimer CADisplayLink 定時器

//在主線程創(chuàng)建的定時器
 NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(calculate) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//當控制器里有scrollView滾動的時候 主線程的runloop會切換到UITrackingRunLoopMode 模式下執(zhí)行runLoop 所以在原來NSDefaultRunLoopMode事件下的定時器就不走了

解決辦法就是
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

//還有如果是在子線程添加的控制器 子線程的runLoop是默認沒有的宁炫。
只有調(diào)用[NSRunLoop currentRunLoop] 系統(tǒng)才會去創(chuàng)建對應(yīng)子線程的runLoop ,只創(chuàng)建了還是沒有用的 還得調(diào)用runLoop的 run發(fā)法讓子線程的runLoop跑起來
這樣加進來的定時器才能夠執(zhí)行

2.performSelector 的使用

大家在調(diào)用performSelector方法的時候 可以點擊option看看蘋果的注釋氮凝。
這個方法是跟RunLoop有關(guān)的
默認是執(zhí)行在當前線程的NSDefaultRunLoopMode 模式下

 NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(creatThread) object:nil];
    _thread = thread;
    [thread start];

- (void)creatThread {
    //創(chuàng)建線程的時候最好加上 自動釋放池
    @autoreleasepool{
        NSPort *port = [[NSPort alloc] init];
        //為了保證runLoop不退出 需要有事件源  這個port就是占位的
        [[NSRunLoop currentRunLoop] addPort:port forMode:NSDefaultRunLoopMode];
        //開啟運行循環(huán)  如果不開啟的話 后期再這個線程是沒法執(zhí)行任務(wù)
        [[NSRunLoop currentRunLoop] run];
    }
}

 [self performSelector:@selector(calculate) onThread:self.thread withObject:nil waitUntilDone:NO];

3.常駐線程

跟第2條一樣 可以常駐線程 用來做一些頻繁的任務(wù)  比如日志的收集上傳  耗時操作的計算 等
運用的經(jīng)典案例 AFNetworking2.0中的 
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
 
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}
- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}

希望我們以后也可以用到常駐線程

4.autoreleaspool 相關(guān)

自動釋放池跟runloop也有關(guān)系
在第一次創(chuàng)建runloop的時候會創(chuàng)建自動釋放池
在runloop休眠的時候會銷毀上次創(chuàng)建的自動釋放池
在被喚醒的時候會創(chuàng)建新的自動釋放池
已達到無用對象的快速釋放 減少內(nèi)存

說了這么多應(yīng)用 我們看看他它的源碼吧

RunLoop大概構(gòu)造
NSRunLoop 和 CFRunLoopRef是一一對應(yīng)的 只不過是不同框架下的
NSRunLoop是基于CFRunLoopRef的的一層OC封裝羔巢,所以了解RunLoop的內(nèi)部結(jié)構(gòu),就需要研究CFRunLoopRef(core Foundation框架)

RunLoop的相關(guān)類
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

CFRunLoopModeRef代表RunLoop的運行模式
每個runLoop跟線程是一對一的
1.一個 RunLoop 包含若干個 Mode,每個Mode又包含若干個Source/Timer/Observer
2.每次RunLoop啟動時朵纷,只能指定其中一個 Mode炭臭,這個Mode被稱作 CurrentMode.
3. 如果需要切換Mode,只能退出Loop袍辞,再重新指定一個Mode進入
4. 這樣做主要是為了分隔開不同組的Source/Timer/Observer,讓其互不影響

CFRunLoopSourceRef 代表事件 Source有兩個版本:Source0 和 Source1常摧。
Source0 只包含了一個回調(diào)(函數(shù)指針)搅吁,它并不能主動觸發(fā)事件。使用時落午,你需要先調(diào)用 CFRunLoopSourceSignal(source)谎懦,將這個 Source 標記為待處理,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop溃斋,讓其處理這個事件界拦。
Source1 包含了一個 mach_port 和一個回調(diào)(函數(shù)指針),被用于通過內(nèi)核和其他線程相互發(fā)送消息梗劫。這種 Source 能主動喚醒 RunLoop 的線程

CFRunLoopTimerRef 是基于時間的觸發(fā)器享甸,它和 NSTimer 是toll-free bridged 的,可以混用梳侨。其包含一個時間長度和一個回調(diào)(函數(shù)指針)蛉威。當其加入到 RunLoop 時,RunLoop會注冊對應(yīng)的時間點走哺,當時間點到時蚯嫌,RunLoop會被喚醒以執(zhí)行那個回調(diào)。

CFRunLoopObserverRef 是觀察者丙躏,每個 Observer 都包含了一個回調(diào)(函數(shù)指針)择示,當 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
};

上面的 Source/Timer/Observer 被統(tǒng)稱為 mode item,一個 item 可以被同時加入多個 mode敢朱。但一個 item 被重復加入同一個 mode 時是不會有效果的剪菱。如果一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出拴签,不進入循環(huán)孝常。

RunLoop的Mode
UIInitializationRunLoopMode 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用蚓哩。
GSEventReceiveRunLoopMode接受系統(tǒng)事件的內(nèi)部 Mode构灸,通常用不到。
kCFRunLoopDefaultModeApp的默認 Mode岸梨,通常主線程是在這個 Mode 下運行的喜颁。
UITrackingRunLoopMode界面跟蹤 Mode稠氮,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響半开。
kCFRunLoopCommonModes這是一個占位的 Mode隔披,沒有實際作用


關(guān)系圖如下


image.png

image.png

蘋果官方文檔
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-131281
CFRunLoop源碼地址
http://opensource.apple.com/tarballs/CF/

#######接下來我們看下源碼

/// 用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 即將進入 loop奢米。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
    
    /// 內(nèi)部函數(shù),進入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 的線程即將進入休眠(sleep)。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }
            
            /// 7. 調(diào)用 mach_msg 等待接受 mach_port 的消息炭序。線程將進入休眠, 直到被下面某一個事件喚醒啤覆。
            /// ? 一個基于 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) {
                /// 進入loop時參數(shù)說處理完事件就返回。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出傳入?yún)?shù)標記的超時時間了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部調(diào)用者強制停止了
                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 就是這樣一個函數(shù)畸颅,其內(nèi)部是一個 do-while 循環(huán)担巩。當你調(diào)用 CFRunLoopRun() 時,線程就會一直停留在這個循環(huán)里没炒;直到超時或被手動停止涛癌,該函數(shù)才會返回。

相關(guān)邏輯圖


image.png
image.png
demo

//demo中有 起死回升術(shù)
// 切記 崩潰的時候不支持xcode調(diào)試 要不你就會以為只能起死回生一次呢
//還可以用來檢測主線程的卡頓等
https://github.com/rjb0514/RunLoop.git

參考blog
//深入理解RunLoop YYKit的作者 https://blog.ibireme.com/2015/05/18/runloop/
iOS—RunLoop深度剖析 http://www.reibang.com/p/de2716807570
//sunnyxx線下分享RunLoop
https://v.youku.com/v_show/id_XODgxODkzODI0.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市拳话,隨后出現(xiàn)的幾起案子先匪,更是在濱河造成了極大的恐慌,老刑警劉巖弃衍,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件呀非,死亡現(xiàn)場離奇詭異,居然都是意外死亡镜盯,警方通過查閱死者的電腦和手機姜钳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來形耗,“玉大人,你說我怎么就攤上這事辙浑〖さ樱” “怎么了?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵判呕,是天一觀的道長倦踢。 經(jīng)常有香客問我,道長侠草,這世上最難降的妖魔是什么辱挥? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮边涕,結(jié)果婚禮上晤碘,老公的妹妹穿的比我還像新娘。我一直安慰自己功蜓,他們只是感情好园爷,可當我...
    茶點故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著式撼,像睡著了一般童社。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上著隆,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天扰楼,我揣著相機與錄音,去河邊找鬼美浦。 笑死弦赖,一個胖子當著我的面吹牛褪那,可吹牛的內(nèi)容都是我干的买鸽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼崭捍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了案腺?” 一聲冷哼從身側(cè)響起庆冕,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎劈榨,沒想到半個月后访递,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡同辣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年拷姿,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片旱函。...
    茶點故事閱讀 39,769評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡响巢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出棒妨,到底是詐尸還是另有隱情踪古,我是刑警寧澤,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布券腔,位于F島的核電站伏穆,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏纷纫。R本人自食惡果不足惜枕扫,卻給世界環(huán)境...
    茶點故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望辱魁。 院中可真熱鬧烟瞧,春花似錦、人聲如沸商叹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽剖笙。三九已至卵洗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間弥咪,已是汗流浹背过蹂。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留聚至,地道東北人酷勺。 一個月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像扳躬,于是被迫代替她去往敵國和親脆诉。 傳聞我的和親對象是個殘疾皇子甚亭,可洞房花燭夜當晚...
    茶點故事閱讀 44,678評論 2 354

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