Runloop的應(yīng)用場(chǎng)景

runloop是iOS開(kāi)發(fā)中比較重要的一個(gè)概念,之前的博客也有總結(jié)過(guò)它的基本概念runloop筆記,不過(guò)很多人包括我,之前也都是只知道其概念,并沒(méi)有去總結(jié)它在實(shí)際開(kāi)發(fā)中的應(yīng)用,這一篇就來(lái)總結(jié)一下它在實(shí)際開(kāi)發(fā)中的運(yùn)用,可能并不全面,后面會(huì)陸續(xù)補(bǔ)充。

1.常駐線程

之前在AFNetworking2.0系列里面一直有這樣的一段代碼。


+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        // 先用 NSThread 創(chuàng)建了一個(gè)線程
        [[NSThread currentThread] setName:@"AFNetworking"];
        // 使用 run 方法添加 runloop
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

AFNetworking 2.0 先用 NSThread 創(chuàng)建了一個(gè)線程,并使用 NSRunLoop 的 run 方法給這個(gè)新線程添加了一個(gè) runloop。

那么它為什么要這么做呢?這是因?yàn)樵贏FNetworking 2.0時(shí),iOS的原生網(wǎng)絡(luò),更準(zhǔn)確的說(shuō)是NSURLConnection還存在一些設(shè)計(jì)上的缺陷。NSURLConnection 發(fā)起請(qǐng)求后,所在的線程需要一直存活,以等待接收NSURLConnectionDelegate回調(diào)方法。但是,網(wǎng)絡(luò)返回的時(shí)間不確定,所以這個(gè)線程就需要一直常駐在內(nèi)存中。如果我們使用主線程來(lái)進(jìn)行這項(xiàng)工作,那么就會(huì)給主線程增加很大的負(fù)擔(dān),所以AFNetworking 2.0就使了一個(gè)常駐的線程,用來(lái)處理請(qǐng)求的發(fā)起與響應(yīng)。

在iOS使用了NSURLSession替代NSURLConnection之后,AFNetworking3.0也就取消了這個(gè)操作,NSURLSession可以指定回調(diào) NSOperationQueue,這樣請(qǐng)求就不需要讓線程一直常駐在內(nèi)存里去等待回調(diào)了。

self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];

NSURLSession 發(fā)起的請(qǐng)求,可以指定回調(diào)的 delegateQueue,不再需要在當(dāng)前線程進(jìn)行代理方法的回調(diào)。所以說(shuō),NSURLSession 解決了 NSURLConnection 的線程回調(diào)問(wèn)題。

我們可以看到,創(chuàng)建常駐線程的方式很簡(jiǎn)單,但是AFNetworking也使用的非常謹(jǐn)慎,因?yàn)槿绻覀兠總€(gè)功能模塊都創(chuàng)建這樣一個(gè)常駐線程來(lái)處理自己的事情,那么一個(gè)APP中可能就會(huì)存在數(shù)十個(gè)常駐線程的運(yùn)行,這樣就會(huì)造成資源的極大浪費(fèi),而不是合理利用。

我自己寫(xiě)了一段代碼驗(yàn)證了一下:

//自定義線程
#import "BZThread.h"

@implementation BZThread

- (void)dealloc{
    NSLog(@"%s",__func__);
}

@end
//執(zhí)行代碼
- (void)threadTest {
    BZThread *thread = [[BZThread alloc] initWithTarget:self selector:@selector(subThreadOpetion) object:nil];
    [thread start];
}

- (void)subThreadOpetion {
    @autoreleasepool {
        NSLog(@"%@----子線程任務(wù)開(kāi)始",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:3.0];
        NSLog(@"%@----子線程任務(wù)結(jié)束",[NSThread currentThread]);
    }
}
//控制臺(tái)輸出
2020-06-13 13:15:07.718009+0800 runloopDemo[3964:993110] <BZThread: 0x2814c1640>{number = 5, name = (null)}----子線程任務(wù)開(kāi)始
2020-06-13 13:15:10.723402+0800 runloopDemo[3964:993110] <BZThread: 0x2814c1640>{number = 5, name = (null)}----子線程任務(wù)結(jié)束
2020-06-13 13:15:10.724248+0800 runloopDemo[3964:993110] -[BZThread dealloc]

可以看到我們的線程在執(zhí)行完任務(wù)就會(huì)自動(dòng)銷(xiāo)毀,再使用常駐線程測(cè)試一下。

- (void)threadTest {
    BZThread *thread = [[BZThread alloc] initWithTarget:self selector:@selector(subThreadEntryPoint) object:nil];
    [thread setName:@"BZThread"];
    [thread start];
    self.subThread = thread;
}

- (void)subThreadEntryPoint {
    @autoreleasepool {
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        // NSLog(@"runLoop--%@", runLoop);
        NSLog(@"啟動(dòng)RunLoop前--%@",runLoop.currentMode);
        [runLoop run];
    }
}

- (void)subThreadOpetion {
    @autoreleasepool {
        NSLog(@"%@----子線程任務(wù)開(kāi)始",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:3.0];
        NSLog(@"%@----子線程任務(wù)結(jié)束",[NSThread currentThread]);
    }
}
//控制臺(tái)輸出
2020-06-13 13:18:29.722947+0800 runloopDemo[3964:993685] 啟動(dòng)RunLoop前--(null)
2020-06-13 13:18:31.308423+0800 runloopDemo[3964:993685] <BZThread: 0x281418b00>{number = 6, name = BZThread}----子線程任務(wù)開(kāi)始
2020-06-13 13:18:34.314521+0800 runloopDemo[3964:993685] <BZThread: 0x281418b00>{number = 6, name = BZThread}----子線程任務(wù)結(jié)束
2020-06-13 13:18:40.421560+0800 runloopDemo[3964:993685] <BZThread: 0x281418b00>{number = 6, name = BZThread}----子線程任務(wù)開(kāi)始
2020-06-13 13:18:43.426793+0800 runloopDemo[3964:993685] <BZThread: 0x281418b00>{number = 6, name = BZThread}----子線程任務(wù)結(jié)束

開(kāi)啟常駐線程之后,這個(gè)線程在執(zhí)行完任務(wù)之后依舊存活,下一次執(zhí)行時(shí)依舊可以使用此線程。

不過(guò)常駐線程還是要慎用,一不小心就會(huì)造成資源的浪費(fèi),即使AFN的大神們也使用的小心翼翼,如果我們確實(shí)有這個(gè)需要,讓線程存活一段時(shí)間,可以使用其他的方法。

如果確實(shí)需要保活線程一段時(shí)間的話,可以選擇使用 NSRunLoop 的另外兩個(gè)方法runUntilDate:runMode:beforeDate,來(lái)指定線程的保活時(shí)長(zhǎng)。讓線程存活時(shí)間可預(yù)期,總比讓線程常駐,至少在硬件資源利用率這點(diǎn)上要更加合理?;蛘撸氵€可以使用 CFRunLoopRef 的 CFRunLoopRun 和 CFRunLoopStop 方法來(lái)完成 runloop 的開(kāi)啟和停止,達(dá)到將線程?;钜欢螘r(shí)間的目的。

2.Timer的不正常計(jì)時(shí)

runloop在運(yùn)行中存在好幾種mode,每次只能選擇一種mode運(yùn)行,runloop為了保證界面的運(yùn)作流暢,有一個(gè)專(zhuān)門(mén)在滑動(dòng)中使用的UITrackingRunLoopMode,而我們主線程中使用定時(shí)器是kCFRunLoopDefaultMode,這樣就導(dǎo)致我們?cè)谝晥D滑動(dòng)時(shí)會(huì)造成定時(shí)器的計(jì)時(shí)不準(zhǔn)確,這個(gè)沖突在開(kāi)發(fā)中影響還是比較大的。

舉一個(gè)最常見(jiàn)的例子,現(xiàn)在的APP中輪播圖是非常常見(jiàn)的一種空間,它一般會(huì)支持自動(dòng)滾動(dòng),那么它滾動(dòng)的間隔就需要靠定時(shí)器來(lái)控制,這樣就無(wú)法保證功能的正常運(yùn)作。我們可以指定Runloop的模式為kCFRunLoopCommonModes即可,因?yàn)閗CFRunLoopCommonModes包含kCFRunLoopDefaultMode

代碼以及效果來(lái)驗(yàn)證一下,我們先不將計(jì)時(shí)器加入到指定runloop中。

- (void)setupTimer{
    [self invalidateTimer];
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(countAdd) userInfo:nil repeats:YES];
    _timer = timer;
    //如果這句代碼注釋掉滑動(dòng)時(shí)計(jì)時(shí)器將不正常
//    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

效果圖如下:


滑動(dòng)中計(jì)時(shí)器停止

可以看到在滑動(dòng)中計(jì)時(shí)器完全處于停止?fàn)顟B(tài)無(wú)法滑動(dòng),再解開(kāi)注釋代碼,加入到NSRunLoopCommonModes中看一下效果,效果圖如下:


計(jì)時(shí)器正常

這樣就可以達(dá)到我們正常計(jì)時(shí)的效果。

3.監(jiān)控卡頓

在開(kāi)發(fā)中交互出現(xiàn)卡頓是很致命的一個(gè)問(wèn)題,造成用戶體驗(yàn)不好就會(huì)很容易失去用戶。一般造成線程卡頓的原因有很多,比如大量的圖文混排,I/O操作,網(wǎng)絡(luò)同步請(qǐng)求等,如果這些操作放在來(lái)主線程上來(lái)做就會(huì)表現(xiàn)為丟幀。蘋(píng)果手機(jī)正常情況下是60幀每秒,我們可以通過(guò)fps來(lái)監(jiān)控卡頓,但是這樣并不準(zhǔn)確,因?yàn)槿绻鹒ps在20-30左右的情況下肉眼還算是流暢,但這時(shí)候其實(shí)已經(jīng)發(fā)生卡頓了。

我們可以利用監(jiān)聽(tīng)runloop的狀態(tài)來(lái)判斷調(diào)用方法是否執(zhí)行時(shí)間過(guò)長(zhǎng),runloop有六種狀態(tài)。

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

如果我們的主線程遲遲無(wú)法進(jìn)入休眠或者喚醒后遲遲無(wú)法進(jìn)入下一個(gè)狀態(tài),就可以認(rèn)為是出現(xiàn)了卡頓,因此我們可以使用觀察者來(lái)觀察kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting這兩個(gè)狀態(tài)來(lái)判斷是否出現(xiàn)了卡頓,因?yàn)榭赡苁撬咧暗腟ource0事件或者喚醒runloop的mach_port事件執(zhí)行受阻。為什么不是觀察kCFRunLoopBeforeWaiting呢?因?yàn)槿绻叩竭@一步說(shuō)明Source0事件已經(jīng)處理完畢了。

那么如何監(jiān)控呢?通過(guò)代碼來(lái)說(shuō)明:

//創(chuàng)建一個(gè)觀察者
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                              kCFRunLoopAllActivities,
                                              YES,
                                              0,
                                              &runLoopObserverCallBack,
                                              &context);
//將觀察者添加到主線程runloop的common模式下的觀察中
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);

首先創(chuàng)建一個(gè)觀察者,將它添加到主線程中進(jìn)行主線程runloop狀態(tài)的觀察。我們?cè)谟^察者的回調(diào)中方法如下:

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    BZStuckMonitor *lagMonitor = (__bridge BZStuckMonitor*)info;
    lagMonitor->runLoopActivity = activity;
    //獲取信號(hào)量的值
    dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
    //信號(hào)通知,dispatch_semaphore_signal使信號(hào)量加1
    dispatch_semaphore_signal(semaphore);
}

當(dāng)主線程runloop的狀態(tài)發(fā)生改變則會(huì)通知信號(hào)量,讓信號(hào)量的值加1,然后我們開(kāi)啟一個(gè)持續(xù)執(zhí)行的loop。

//創(chuàng)建子線程監(jiān)控
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //子線程開(kāi)啟一個(gè)持續(xù)的loop用來(lái)進(jìn)行監(jiān)控
        while (YES) {
            long semaphoreWait = dispatch_semaphore_wait(self->dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 10*NSEC_PER_MSEC));
            //  semaphoreWait 的值不為 0, 說(shuō)明線程被堵塞
            if (semaphoreWait != 0) {
                if (!self->runLoopObserver) {
                    self->dispatchSemaphore = 0;
                    self->runLoopActivity = 0;
                    return;
                }
                // BeforeSources和 AfterWaiting 這兩個(gè) runloop 狀態(tài)的區(qū)間時(shí)間能夠檢測(cè)到是否卡頓
                if (self->runLoopActivity == kCFRunLoopBeforeSources || self->runLoopActivity == kCFRunLoopAfterWaiting) {
                    // 將堆棧信息上報(bào)服務(wù)器的代碼放到這里
                    NSLog(@"卡頓了");
                } //end activity
            }// end semaphore wait
        }// end while
    });

dispatch_semaphore_wait方法會(huì)將信號(hào)量的值減1,如果減完之后信號(hào)量的值為0則可以繼續(xù)執(zhí)行,如果不為0則會(huì)阻塞,至于阻塞多久合適呢?如果這個(gè)值設(shè)置為40毫秒,那用戶肉眼就已經(jīng)能明顯感知有卡頓了,所以不能精準(zhǔn)的測(cè)試出卡頓,我個(gè)人認(rèn)為設(shè)置為10毫秒可能測(cè)試出來(lái)比較準(zhǔn)確。

為了測(cè)試我寫(xiě)了一個(gè)很長(zhǎng)的UICollectionView,然后讓其加載很大的本地圖片,這樣就會(huì)出現(xiàn)主線程解壓縮步驟,而且還為cell繪制圓角陰影等,然后快速滑動(dòng)。

效果如下:


界面卡頓

控制臺(tái)打印如下:

2020-06-13 14:23:01.882126+0800 runloopDemo[4018:1000055] 卡頓了
2020-06-13 14:23:01.888794+0800 runloopDemo[4018:1000055] 卡頓了
2020-06-13 14:23:01.895314+0800 runloopDemo[4018:1000055] 卡頓了
2020-06-13 14:23:01.901711+0800 runloopDemo[4018:1000055] 卡頓了
2020-06-13 14:23:01.908091+0800 runloopDemo[4018:1000055] 卡頓了
2020-06-13 14:23:01.914493+0800 runloopDemo[4018:1000055] 卡頓了

可以配合使用打印堆棧的工具來(lái)獲取卡頓時(shí)正在執(zhí)行的方法,這一塊可以單獨(dú)拿出來(lái)總結(jié)一下,以后會(huì)驗(yàn)證總結(jié)再發(fā)上來(lái)。

目前這三種就是開(kāi)發(fā)中比較常見(jiàn)的runloop的使用方式,這一篇博客使用到的所有demo都在這里。

參考文章:

如何利用 RunLoop 原理去監(jiān)控卡頓?
NSURLSession與NSURLConnection區(qū)別

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

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