iOS-Runloop原理與應(yīng)用

Runloop:運行循環(huán)-死循環(huán)

主要目的:提高性能,有事情就干,沒事情休眠。
參考https://blog.csdn.net/callauxiliary/article/details/107419854

主要應(yīng)用

1,保證線程一直運行,處理事件,比如觸摸事件,時鐘事件,都是由runloop完成。
2,優(yōu)化卡頓:將一次runloop執(zhí)行完的任務(wù),放到多次runloop中執(zhí)行。
3,UI滑動時計時不準(zhǔn)確的問題,設(shè)置定時器的Mode為:NSRunLoopCommonModes。
4,需要在線程上使用performSelector*****方法(運行時方法)。例如

讓UITableView、UICollectionView等延遲加載圖片

[imageView performSelector:@selector(setImage:) withObject:image afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

這算一個優(yōu)化點,這里用的defaultMode,就是滾動的時候不加載圖片,停止?jié)L動加載圖片

Runloop運行原理

當(dāng)你調(diào)用 CFRunLoopRun() 時,線程就會一直停留在這個循環(huán)里;直到超時或被手動停止,該函數(shù)才會返回。每次線程運行RunLoop都會自動處理之前未處理的消息,并且將消息發(fā)送給觀察者,讓事件得到執(zhí)行。
Runloop的生命周期:在第一次獲取時創(chuàng)建,在線程結(jié)束時銷毀。

Runloop要想跑起來,它的內(nèi)部必須要有一個mode,這個mode里面必須有source\observer\timer,至少要有其中的一個。
系統(tǒng)默認(rèn)注冊了5個mode

    a.kCFRunLoopDefaultMode:App的默認(rèn)Mode,通常主線程是在這個Mode下運行

        b.UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響

        c.UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用

        d.GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到

        e.kCFRunLoopCommonModes: 這是一個占位用的Mode,不是一種真正的Mode

RunLoop運行時首先根據(jù)modeName找到對應(yīng)mode,如果mode里沒有source/timer/observer,直接返回。

主要分為三大步驟:

1、首先根據(jù)modeName找到對應(yīng)的Mode

2、如果model里沒有Source/Timer/Observer,直接返回

3、如果model有Source/Timer/Observer,就會即將進入runloop


image.png

詳細步驟

簡單來講就是一個do-while循環(huán),有輸入源就喚醒,沒有就處于休眠狀態(tài),官方解釋鏈接。具體過程如下:

  1. 通知觀察者開始進入 Runloop
  2. 通知觀察者開始處理 Timer 事件
  3. 通知觀察者將要處理非基于 port 的事件
  4. 啟動準(zhǔn)備好的事件
  5. 如果基于 port 的事件已經(jīng)準(zhǔn)備好,立即啟動。并進入步驟 9
  6. 通知觀察者進入休眠狀態(tài)
  7. 線程進入休眠直到以下事件出現(xiàn)
    • 基于 port 的事件源出現(xiàn)
    • 定時器啟動
    • 設(shè)置的時間已經(jīng)超時
    • RunLoop 被喚醒
  8. 通知觀察者線程將被喚醒
  9. 處理已經(jīng)進入的事件
    • 如果用戶自定義的計時器啟動,處理事件并重啟 Runloop。轉(zhuǎn)到第二步
    • 輸入源啟動,傳遞消息
    • 如果 Runloop 被顯示喚醒且沒有超時,重啟 Runloop。轉(zhuǎn)到第二步
  10. 通知觀察者 Runloop 已經(jīng)退出

Runloop組成

兩種對象:NSRunLoop 和 CFRunLoopRef。

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

RunLoop共包含5個類,但公開的只有Source、Timer、Observer相關(guān)的三個類。
1,Timder,時鐘
2,source,事件源:一切事件的來源,按照函數(shù)的調(diào)用棧分為source0:非系統(tǒng)內(nèi)核事件,source1:系統(tǒng)內(nèi)核事件。
3,observer,觀察者,觀察的runloop的循環(huán)周期。

蘋果不允許直接創(chuàng)建 RunLoop,它只提供了兩個自動獲取的函數(shù):CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。

APP啟動過程

1,當(dāng)點擊APP時,操作系統(tǒng)開啟一條線程執(zhí)行程序的main函數(shù)。這個線程就是這個程序的主線程。這個線程是一個常駐線程,因為這個線程的Runloop被開啟了,不會被線程池釋放。

線程和runloop 的關(guān)系

線程和 RunLoop 之間是一一對應(yīng)的,其關(guān)系是保存在一個全局的 Dictionary 里。線程剛創(chuàng)建時并沒有 RunLoop,如果你不主動獲取,那它一直都不會有。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時,RunLoop 的銷毀是發(fā)生在線程結(jié)束時。你只能在一個線程的內(nèi)部獲取其 RunLoop(主線程除外)

Runloop的作用

1,保證線程不退出。
2,監(jiān)聽所有的事件,比如觸摸事件,時鐘事件,網(wǎng)絡(luò)事件等。
子線程要監(jiān)聽事件,就必須開啟子線程的runloop。

驗證將時鐘添加到runloop才可以實現(xiàn)監(jiān)聽

1,scheduledTimerWithTimeInterval初始化方法默認(rèn)是添加到當(dāng)前線程的runloop的,所以不用顯示的添加。
這種方法添加是NSDefaultRunLoopMode模式,UI事件會中斷響應(yīng)。

-(void)test{

    [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
    
}

-(void)timeerFunC{
    static int num;
    NSLog(@"%d",num);
    num++;
}

3,子線程結(jié)束runloop

-(void)test{
    //要放在子線程,設(shè)置runloop時間,時間過了沒有執(zhí)行的任務(wù),子線程就會結(jié)束,并釋放
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
        //將時鐘手動添加到當(dāng)前的runloop,否者時鐘方法不會執(zhí)行
        [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
        //當(dāng)為yes時,取消RunLoop循環(huán)
        while (!_finish) {
            NSLog(@"來了11");
            [[NSRunLoop currentRunLoop]runUntilDate:[NSDate dateWithTimeIntervalSinceReferenceDate:1]];
        }
        NSLog(@"來了");
    });
}

-(void)timeerFunC{
    static int num;
    NSLog(@"%d",num);
    num++;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"22222");
    _finish = YES;
}

2,timerWithTimeInterval初始化方法需要手動將時鐘添加到當(dāng)前線程的runloop中,否則時鐘不能被監(jiān)聽。

-(void)test{
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
    //將時鐘手動添加到當(dāng)前的runloop,否者時鐘方法不會執(zhí)行
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
}

-(void)timeerFunC{
    static int num;
    NSLog(@"%d",num);
    num++;
}

runloop的三種Mode

1,當(dāng)對象使用NSDefaultRunLoopMode(默認(rèn)模式,一般處理網(wǎng)絡(luò)事件和Timer事件),在runloop執(zhí)行UI事件的時候,會暫停響應(yīng)該對象的事件。
2,當(dāng)對象使用UITrackingRunLoopMode(UI模式,一般處理UI事件),只有在runloop執(zhí)行UI事件的時候,才會會暫停響應(yīng)該對象的事件。
3,當(dāng)對象使用NSRunLoopCommonModes(占位模式,這個不是一個真的模式,只是同時添加了上面兩種模式),在runloop執(zhí)行UI事件的時候,也會響應(yīng)該對象的事件(最理想的結(jié)果)。

開啟當(dāng)前線程的runloop

子線程處理需要監(jiān)聽的事件(需要子線程活著一直監(jiān)聽的情況),可以啟動子線程的runloop,讓子線程成為常駐線程,避免被釋放,從而可以響應(yīng)監(jiān)聽的事件。唯一讓線程不被釋放的方法就去啟動他的runloop。
解決NSTimer在滑動時停止工作的問題的辦法
1,設(shè)置定時器的Mode為:NSRunLoopCommonModes
2,將NSTimder放到子線程并且開啟runloop,name就是算是NSDefaultRunLoopMode,滑動UI也不會影響Timer。
NSTimder處理的事件一般放在子線程,并開啟子線程的runloop,避免影響主線程處理事件。

-(void)test{

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
        //將時鐘手動添加到當(dāng)前的runloop,否者時鐘方法不會執(zhí)行
        [[NSRunLoop currentRunLoop]addTimer:timer forMode: NSDefaultRunLoopMode ];
        //手動開啟當(dāng)前線程的runloop,這是一個死循環(huán)
        [[NSRunLoop currentRunLoop]run];
        NSLog(@"這里不會走了");
    });
}

-(void)timeerFunC{
    static int num;
    NSLog(@"%d",num);
    num++;
}

GCD實現(xiàn)定時器

Dispatch Source創(chuàng)建定時器timer,優(yōu)于NSTimer
必須要聲明timer屬性強應(yīng)用,不然會被釋放

@property (nonatomic, strong) dispatch_source_t timer;
-(void)test2{
    NSLog(@"啟動");
    _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
    dispatch_source_set_event_handler(_timer, ^{
        NSLog(@"間隔一秒執(zhí)行%@",[NSThread currentThread]);
    });
    dispatch_resume(_timer);
    NSLog(@"啟動");
}

優(yōu)化練習(xí)

tableview在快速滑動時,高清圖片造成卡頓優(yōu)化。
原因:tableview在快速滑動時,runloop必須在一次循環(huán)內(nèi),渲染所有的圖片。
思路:runloop每次循環(huán)只加載一張圖片,例如一個屏幕總的可以顯示15張圖片,那么就分15次加入到runloop中加載。相當(dāng)于把一次做完的事情,分成了15次。例如監(jiān)聽kCFRunLoopBeforeWaiting事件,回調(diào)函數(shù)加載圖片,加載完成后刪除這個任務(wù)。每次加載圖片后都會再次到達kCFRunLoopBeforeWaiting,還有圖片任務(wù)就會繼續(xù)執(zhí)行。
通過CFRunLoopObserverRef監(jiān)聽runloop 的狀態(tài),函數(shù)指針回調(diào),Ref是引用,指針的意思。
步驟
1,添加runloop觀察者,觀察runloop 的狀態(tài)變化。
2,將加載圖片的代碼塊放到數(shù)組中。
3,在觀察者回調(diào)中,拿出數(shù)組中加載圖片的代碼執(zhí)行。

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

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