iOS線上實(shí)時(shí)卡頓監(jiān)控

轉(zhuǎn)載自:iOS實(shí)時(shí)卡頓監(jiān)控

在移動(dòng)設(shè)備上開(kāi)發(fā)軟件,性能一直是我們最為關(guān)心的話題之一,我們作為程序員除了需要努力提高代碼質(zhì)量之外,及時(shí)發(fā)現(xiàn)和監(jiān)控軟件中那些造成性能低下的”罪魁禍?zhǔn)住币彩俏覀兩袷サ穆氊?zé).

眾所周知,iOS平臺(tái)因?yàn)閁IKit本身的特性,需要將所有的UI操作都放在主線程執(zhí)行,所以也造成不少程序員都習(xí)慣將一些線程安全性不確定的邏輯,以及其它線程結(jié)束后的匯總工作等等放到了主線,所以主線程中包含的這些大量計(jì)算、IO、繪制都有可能造成卡頓.

在Xcode中已經(jīng)集成了非常方便的調(diào)試工具Instruments,它可以幫助我們?cè)陂_(kāi)發(fā)測(cè)試階段分析軟件運(yùn)行的性能消耗,但一款軟件經(jīng)過(guò)測(cè)試流程和實(shí)驗(yàn)室分析肯定是不夠的,在正式環(huán)境中由大量用戶在使用過(guò)程中監(jiān)控、分析到的數(shù)據(jù)更能解決一些隱藏的問(wèn)題.

尋找卡頓的切入點(diǎn)

監(jiān)控卡頓,最直接就是找到主線程都在干些啥玩意兒.我們知道一個(gè)線程的消息事件處理都是依賴于NSRunLoop來(lái)驅(qū)動(dòng),所以要知道線程正在調(diào)用什么方法,就需要從NSRunLoop來(lái)入手.CFRunLoop的代碼是開(kāi)源,可以在此處查閱到源代碼http://opensource.apple.com/source/CF/CF-1151.16/CFRunLoop.c,其中核心方法CFRunLoopRun簡(jiǎn)化后的主要邏輯大概是這樣的:

int32_t __CFRunLoopRun()
{
    //通知即將進(jìn)入runloop
    __CFRunLoopDoObservers(KCFRunLoopEntry);
    do {
        // 通知將要處理timer和source
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
        
        __CFRunLoopDoBlocks();  //處理非延遲的主線程調(diào)用
        __CFRunLoopDoSource0(); //處理UIEvent事件
        
        //GCD dispatch main queue
        CheckIfExistMessagesInMainDispatchQueue();
        
        // 即將進(jìn)入休眠
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
        
        // 等待內(nèi)核mach_msg事件
        mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
        
        // Zzz...
        
        // 從等待中醒來(lái)
        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
        
        // 處理因timer的喚醒
        if (wakeUpPort == timerPort) {
            __CFRunLoopDoTimers();
        
        // 處理異步方法喚醒,如dispatch_async
        }else if (wakeUpPort == mainDispatchQueuePort){
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
            
            // UI刷新,動(dòng)畫(huà)顯示
        }else{
                __CFRunLoopDoSource1();
        }
        
        // 再次確保是否有同步的方法需要調(diào)用
        __CFRunLoopDoBlocks();
        
    } while (!stop && !timeout);
    
    //通知即將退出runloop
    __CFRunLoopDoObservers(CFRunLoopExit);
}

不難發(fā)現(xiàn)NSRunLoop調(diào)用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之間,還有kCFRunLoopAfterWaiting之后,也就是如果我們發(fā)現(xiàn)這兩個(gè)時(shí)間內(nèi)耗時(shí)太長(zhǎng),那么就可以判定出此時(shí)主線程卡頓.

量化卡頓的程度

要監(jiān)控NSRunLoop的狀態(tài),我們需要使用到CFRunLoopObserverRef,通過(guò)它可以實(shí)時(shí)獲得這些狀態(tài)值的變化,具體的使用如下:

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    MyClass *object = (__bridge MyClass*)info;
    object->activity = activity;
}

- (void)registerObserver
{
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                            kCFRunLoopAllActivities,
                                                            YES,
                                                            0,
                                                            &runLoopObserverCallBack,
                                                            &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}

只需要另外再開(kāi)啟一個(gè)線程,實(shí)時(shí)計(jì)算這兩個(gè)狀態(tài)區(qū)域之間的耗時(shí)是否到達(dá)某個(gè)閥值,便能揪出這些性能殺手.

為了讓計(jì)算更精確,需要讓子線程更及時(shí)的獲知主線程N(yùn)SRunLoop狀態(tài)變化,所以dispatch_semaphore_t是個(gè)不錯(cuò)的選擇,另外卡頓需要覆蓋到多次連續(xù)小卡頓和單次長(zhǎng)時(shí)間卡頓兩種情景,所以判定條件也需要做適當(dāng)優(yōu)化.將上面兩個(gè)方法添加計(jì)算的邏輯如下:

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    MyClass *object = (__bridge MyClass*)info;
    
    // 記錄狀態(tài)值
    object->activity = activity;
    
    // 發(fā)送信號(hào)
    dispatch_semaphore_t semaphore = moniotr->semaphore;
    dispatch_semaphore_signal(semaphore);
}

- (void)registerObserver
{
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                            kCFRunLoopAllActivities,
                                                            YES,
                                                            0,
                                                            &runLoopObserverCallBack,
                                                            &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    
    // 創(chuàng)建信號(hào)
    semaphore = dispatch_semaphore_create(0);
    
    // 在子線程監(jiān)控時(shí)長(zhǎng)
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES)
        {
            // 假定連續(xù)5次超時(shí)50ms認(rèn)為卡頓(當(dāng)然也包含了單次超時(shí)250ms)
            long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
            if (st != 0)
            {
                if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
                {
                    if (++timeoutCount < 5)
                        continue;
                    
                    NSLog(@"好像有點(diǎn)兒卡哦");
                }
            }
            timeoutCount = 0;
        }
    });
}
記錄卡頓的函數(shù)調(diào)用

監(jiān)控到了卡頓現(xiàn)場(chǎng),當(dāng)然下一步便是記錄此時(shí)的函數(shù)調(diào)用信息,此處可以使用一個(gè)第三方Crash收集組件PLCrashReporter,它不僅可以收集Crash信息也可用于實(shí)時(shí)獲取各線程的調(diào)用堆棧,使用示例如下:

PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD
                                                                   symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];

NSData *data = [crashReporter generateLiveReport];
PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter
                                                          withTextFormat:PLCrashReportTextFormatiOS];
NSLog(@"------------\n%@\n------------", report);

當(dāng)檢測(cè)到卡頓時(shí),抓取堆棧信息,然后在客戶端做一些過(guò)濾處理,便可以上報(bào)到服務(wù)器,通過(guò)收集一定量的卡頓數(shù)據(jù)后經(jīng)過(guò)分析便能準(zhǔn)確定位需要優(yōu)化的邏輯,至此這個(gè)實(shí)時(shí)卡頓監(jiān)控就大功告成了!

文章示例代碼下載:PerformanceMonitor.zip

最后編輯于
?著作權(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ù)。

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

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