iOS- Runloop淺談

最近項(xiàng)目完成年前的時(shí)間也比較輕松,對一些知識就行總結(jié)。今天總結(jié)的是Runloop.
對于Runloop,在平時(shí)項(xiàng)目中用的多嗎?為毛,有時(shí)候一個(gè)項(xiàng)目都看不見一個(gè)Runloop的關(guān)鍵詞,但面試還一直不斷的問?那是因?yàn)槲覀兤鋵?shí)使用了它,但它并不是以NSRunloop出現(xiàn)的,而是以其他的一些形式出現(xiàn)。

我們就以最明顯的主線程來引出Runloop

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

這段代碼絕對見過吧,但是點(diǎn)進(jìn)去看過它具體是怎么實(shí)現(xiàn)的嗎?
就和網(wǎng)上寫的方法,我們打印一下看看結(jié)果。

NSLog(@"開始");
    int num=UIApplicationMain(argc, argv, nil, appDelegateClassName);
    NSLog(@"結(jié)束");

輸出為:2021-01-29 11:11:00.388412+0800 RuntimeDemo[1174:38507] 開始

沒打印出結(jié)束,這說明這個(gè)函數(shù)它就一直沒結(jié)束。具體為什么沒執(zhí)行完的原因是下面這段代碼。

void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

看不懂do里面的代碼沒關(guān)系,到do-while總能看的懂吧,這是不是就可以回答了為什么一直沒結(jié)束了。因?yàn)樗莻€(gè)死循環(huán)啊,就算地球爆炸它也會一直執(zhí)行下去的啊。
好,這算是引出Runloop了,下面我們從易到難來看看Runloop的實(shí)現(xiàn)和底層原理。

什么是Runloop

runloop是通過內(nèi)部維護(hù)的事件循環(huán)來對事件/消息進(jìn)行管理的對象。

Runloop的使用

先來個(gè)簡單的代碼實(shí)現(xiàn)

self.num=0;
    NSTimer *time=[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timebtnclick) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop]addTimer:time forMode:NSDefaultRunLoopMode];
-(void)timebtnclick{
    
    self.num++;
    NSLog(@"---%ld",self.num);
}

這就是一個(gè)簡單的runloop。上面的NSTime不用說,我們來看看添加time到runloop時(shí)的參數(shù)。
1.[[NSRunLoop currentRunLoop] :獲取當(dāng)前runloop,在這也沒創(chuàng)建其他的線程和runloop,在上面提到了,app主線程本身就是一個(gè)死循環(huán)的runloop,所以獲取的這個(gè)必然就是主任務(wù)。
2.time:這個(gè)就是所需的時(shí)間。至于為什么要添加time,先不著急,下面會一張圖就明白了。
3.Mode:NSDefaultRunLoopMode NSRunLoopCommonModes UITrackingRunLoopMode
我們先來看上面三個(gè)model。

假如現(xiàn)在在uiview視圖上面添加一個(gè)滑動視圖,然后就會發(fā)現(xiàn)在滑動這個(gè)視圖的時(shí)候,是不會打印出結(jié)果的。下面先來看這個(gè)圖
截屏2021-01-29 下午3.10.20.png

我們設(shè)置的是默認(rèn)模式NSDefaultRunLoopMode,當(dāng)滑動視圖的時(shí)候,它進(jìn)入的是UI模式的也就是UITrackingRunLoopMode,所以它肯定就不會有打印出來。 UI模式它的調(diào)起是有UI的點(diǎn)擊事件觸發(fā)的。
如果想要滑動的時(shí)候也運(yùn)行runloop,我們是不是可以這樣寫
self.num=0;
    NSTimer *time=[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timebtnclick) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop]addTimer:time forMode:UITrackingRunLoopMode];
    [[NSRunLoop currentRunLoop]addTimer:time forMode:NSDefaultRunLoopMode];

同時(shí)把time添加到了默認(rèn)模式和UI模式。這個(gè)運(yùn)行起來也沒什么問題。但 NSRunLoopCommonModes可能更方便。

self.num=0;
    NSTimer *time=[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timebtnclick) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop]addTimer:time forMode:NSRunLoopCommonModes];
Runloop應(yīng)用

還是和之前一樣,我們先來看它的應(yīng)用然后在看底層的原理

常駐線程
子線程執(zhí)行完操作以后就會立即釋放。即使強(qiáng)引用子線程也不能給子線程添加操作。下面我們來看段代碼

-(IBAction)btnclick:(UIButton *)sender{
    
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    //創(chuàng)建子線程并開啟
    NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil];
    self.thread=thread;
    [thread start];
    
}
-(void)show{
    
    //添加一個(gè)端口
    [[NSRunLoop currentRunLoop]addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    
//    //添加timer
//    NSTimer *timer=[NSTimer scheduledTimerWithTimeInterval:2.0f target:self selector:@selector(test) userInfo:nil repeats:YES];
//    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    //創(chuàng)建監(jiān)聽者
    CFRunLoopObserverRef observer=CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
       
        switch (activity) {
                    case kCFRunLoopEntry:
                        NSLog(@"RunLoop進(jìn)入");
                        break;
                    case kCFRunLoopBeforeTimers:
                        NSLog(@"RunLoop要處理Timers了");
                        break;
                    case kCFRunLoopBeforeSources:
                        NSLog(@"RunLoop要處理Sources了");
                        break;
                    case kCFRunLoopBeforeWaiting:
                        NSLog(@"RunLoop要休息了");
                        break;
                    case kCFRunLoopAfterWaiting:
                        NSLog(@"RunLoop醒來了");
                        break;
                    case kCFRunLoopExit:
                        NSLog(@"RunLoop退出了");
                        break;

                    default:
                        break;
                }
    });
    //添加監(jiān)聽者
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    //開啟runloop
    [[NSRunLoop currentRunLoop]run];
    CFRelease(observer);
}

-(void)test{
    NSLog(@"%@",[NSThread currentThread]);
}

點(diǎn)擊屏幕開啟子線程并開啟Runloop,然后點(diǎn)擊button,我們看看打印結(jié)果

2021-03-19 16:18:15.170791+0800 RuntimeDemo[37765:14285242] RunLoop進(jìn)入
2021-03-19 16:18:15.171060+0800 RuntimeDemo[37765:14285242] RunLoop要處理Timers了
2021-03-19 16:18:15.171230+0800 RuntimeDemo[37765:14285242] RunLoop要處理Sources了
2021-03-19 16:18:15.171343+0800 RuntimeDemo[37765:14285242] RunLoop要休息了
2021-03-19 16:18:31.043323+0800 RuntimeDemo[37765:14285242] RunLoop醒來了
2021-03-19 16:18:31.043712+0800 RuntimeDemo[37765:14285242] RunLoop要處理Timers了
2021-03-19 16:18:31.043828+0800 RuntimeDemo[37765:14285242] RunLoop要處理Sources了
2021-03-19 16:18:31.044092+0800 RuntimeDemo[37765:14285242] <NSThread: 0x283b1d540>{number = 7, name = (null)}
2021-03-19 16:18:31.044213+0800 RuntimeDemo[37765:14285242] RunLoop退出了
2021-03-19 16:18:31.044474+0800 RuntimeDemo[37765:14285242] RunLoop進(jìn)入
2021-03-19 16:18:31.044588+0800 RuntimeDemo[37765:14285242] RunLoop要處理Timers了
2021-03-19 16:18:31.044681+0800 RuntimeDemo[37765:14285242] RunLoop要處理Sources了
2021-03-19 16:18:31.044776+0800 RuntimeDemo[37765:14285242] RunLoop要休息了

發(fā)現(xiàn)runloop處于休眠狀態(tài)。
圖片下載
把setImage放到NSDefaultRunLoopMode去做,在滑動的時(shí)候并不會去調(diào)用復(fù)制圖片的方法,而是等到歡動完畢切換到NSDefaultRunLoopMode下才去調(diào)用。

[self.img performSelector:@selector(setImage:) withObject:image afterDelay:0 inModes:[NSDefaultRunLoopMode]];
//上面?zhèn)鬟f過來的image
-(void)setImage:(UIImgae *)image{

}

滾動的ScrollView導(dǎo)致定時(shí)器失效
在界面上有一個(gè)滾動視圖,如果執(zhí)行一個(gè)定時(shí)器執(zhí)行時(shí)間,在滾動過程中定時(shí)器會失效。這個(gè)時(shí)候,我們可以把timer注冊到NSRunLoopCommonModes,[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
使用GCD創(chuàng)建定時(shí)器,GCD創(chuàng)建的定時(shí)器不會受到runloop的影響
監(jiān)測卡頓
卡頓檢測在這里先不說,我把它規(guī)劃到性能優(yōu)化中去了。
自動釋放池
Runloop內(nèi)部有一個(gè)自動釋放池,當(dāng)Runloop開啟時(shí),就會自動創(chuàng)建一個(gè)自動釋放池,當(dāng)Runloop休眠之前會釋放掉自動釋放池的東西,然后重新創(chuàng)建一個(gè)新的空的自動釋放池,當(dāng)runloop被喚醒執(zhí)行時(shí)Timer source等新的時(shí)間會被放到新的自動釋放池中,當(dāng)runloop退出的時(shí)候也會被釋放。
只有主線程的runloop會默認(rèn)啟動,意味著會自動創(chuàng)建自動釋放池,子線程需要在線程調(diào)度方法中手動的添加自動釋放池。

Runloop的底層原理

Runloop是什么?
Runloop可以理解為運(yùn)行循環(huán),是線程內(nèi)一個(gè)運(yùn)行時(shí)間處理和響應(yīng)傳入事件的一個(gè)循環(huán)。它的作用就是為了在有事件到達(dá)時(shí)喚醒線程一處理各種事件,事件處理完讓進(jìn)入休眠節(jié)省CPU資源。
runloop和線程的關(guān)系
線程的作用是用來執(zhí)行一個(gè)或者多個(gè)任務(wù)的,在默認(rèn)情況下,線程執(zhí)行完之后就會銷毀,那假如我不希望它銷毀,該怎么辦呢?這也就是我們在一開始提到的線程?;?。
1.一條線程對應(yīng)一個(gè)Runloop對象,每條線程都有唯一一個(gè)和他對應(yīng)的Runloop對象
2.主線程的Runloop系統(tǒng)已經(jīng)自動創(chuàng)建好了,子線程的Runloop需要主動創(chuàng)建
3.Runloop在第一獲取時(shí)創(chuàng)建,在線程結(jié)束時(shí)銷毀。
4.Runloop并不保證線程的安全。我們只能在當(dāng)前線程內(nèi)部操作當(dāng)前項(xiàng)城的runloop對象,而不能再當(dāng)前線程內(nèi)部去操作其他線程的runloop對象。
上面說了runloop和線程的關(guān)系,下面我們來看看runloop的中的幾個(gè)相關(guān)類。

runloop結(jié)構(gòu).png

這張圖是不是經(jīng)常見到,它很好的描述了runloop幾個(gè)相關(guān)類的關(guān)系。

CFRunLoopRef:代表 RunLoop 的對象
CFRunLoopModeRef:代表 RunLoop 的運(yùn)行模式
CFRunLoopSourceRef:就是 RunLoop 模型圖中提到的輸入源 / 事件源
CFRunLoopTimerRef:就是 RunLoop 模型圖中提到的定時(shí)源
CFRunLoopObserverRef:觀察者,能夠監(jiān)聽 RunLoop 的狀態(tài)改變

一個(gè)Runloop對象包含了若干個(gè)運(yùn)行模式Model,而在運(yùn)行模式下有包含了若干個(gè)輸入源(CFRunLoopSourceRef)、定時(shí)源、觀察者。
每次runloop啟動時(shí),只能指定其中一個(gè)運(yùn)行模式,這個(gè)運(yùn)行模式被稱作當(dāng)前運(yùn)行模式。
如果需要切換運(yùn)行模式,只能退出當(dāng)前的loop,在重新指定一個(gè)運(yùn)行模式。

知道了runloop的結(jié)構(gòu),我們來具體看看每一個(gè)具體是什么?又是怎么實(shí)現(xiàn)的。
Observer事件
是觀察者,每個(gè) Observer都包含了一個(gè)回調(diào)(函數(shù)指針),當(dāng) RunLoop 的狀態(tài)發(fā)生變化時(shí),觀察者就能通過回調(diào)接受到這個(gè)變化??梢杂^察的時(shí)間點(diǎn)有下面幾個(gè):

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

Timer事件
延遲的performSelector,延遲的dispatch_after,基于時(shí)間的觸發(fā)器,可以與NSTimer進(jìn)行轉(zhuǎn)換
Source事件
1.Source0事件:非基于port的處理事件,不能主動喚醒休眠中的runloop,需要手動觸發(fā)。觸摸屏幕時(shí),屏幕表面的事件會先包裝成Event,event先告訴source1,source1喚醒runloop,然后將事件event分發(fā)給source0,然后由source0來處理。
2.Source1事件:基于mach_port的,來自系統(tǒng)內(nèi)核或其他進(jìn)程或線程的事件,可以主動的喚醒休眠中的Runloop
block事件
費(fèi)延遲的performSelector,非延遲的dispatch_after,block回調(diào)。
Main_Dispatch_Queue事件
GCD中main queue的block會被dispatch到main loop執(zhí)行。
知道了上面這些知識點(diǎn),我們再來看下Runloop的處理邏輯流程圖。

流程圖.png

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、RunLoop初探 1.1、RunLoop是什么? RunLoop從字面上來說是跑圈的意思,如果這樣理解不免有...
    風(fēng)緊扯呼閱讀 812評論 0 5
  • Runloop概述 runloop是來做什么的?runloop和線程有什么關(guān)系?主線程默認(rèn)開啟了runloop么?...
    ProgramDouglas閱讀 1,177評論 0 0
  • iOS基礎(chǔ)全面分析之一(KVC全面分析)iOS基礎(chǔ)全面分析之二(RunLoop全面分析)iOS基礎(chǔ)全面分析之三(K...
    struggle3g閱讀 1,068評論 0 11
  • Runloop:運(yùn)行循環(huán)-死循環(huán) 主要目的:提高性能,有事情就干,沒事情休眠。參考https://blog.csd...
    CDLOG閱讀 754評論 0 0
  • RunLoop概念 一個(gè)APP之所以能在程序運(yùn)行起來不停止,就是RunLoop的原因,RunLoop就像一個(gè)死循環(huán)...
    宙斯YY閱讀 498評論 0 2

友情鏈接更多精彩內(nèi)容