iOS 性能監控(二)—— 主線程卡頓監控

前言:
最近,在看戴銘老師關於 「性能監控」 相關的技術分享,感受收穫不少。基於最近的學習,總結了一些性能監控相關的實踐,並計劃落地一系列 「性能監控」 相關的文章。

目錄以下:
iOS 性能監控(一)—— CPU功耗監控
iOS 性能監控(二)—— 主線程卡頓監控
iOS 性能監控(三)—— 方法耗時監控git


本篇將介紹iOS性能監控工具(QiLagMonitor)中與 「線程卡頓監控」 相關的功能模塊。github

1、瞭解線程的狀態

主線程runloop默認註冊了五個modekCFRunLoopDefaultModeUITrackingRunLoopModeUIInitializationRunLoopModeGSEventReceiveRunLoopModekCFRunLoopCommonModesobjective-c

名稱 做用
kCFRunLoopDefaultMode App的默認 Mode,一般主線程是在這個 Mode 下運行的。
UITrackingRunLoopMode 界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響。
UIInitializationRunLoopMode 剛啓動 App 時第進入的第一個 Mode,啓動完成後就再也不使用。
GSEventReceiveRunLoopMode 接受系統事件的內部 Mode,一般用不到。
kCFRunLoopCommonModes 這是一個佔位的 Mode。其實就是Default模式和UI模式之間切換使用。

其中,APPLE公開提供的Mode有兩個:NSDefaultRunLoopModekCFRunLoopDefaultMode)、NSRunLoopCommonModeskCFRunLoopCommonModes)。app

而咱們的下節要介紹的主線程監控就是使用的就是NSRunLoopCommonModeskCFRunLoopCommonModes)。async

而後,runloop觀察者:Runloop Observer7種狀態。ide

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製代碼

分別表示:工具

狀態 含義
kCFRunLoopEntry 運行runloop的入口。
kCFRunLoopBeforeTimers Inside the event processing loop before any timers are processed.(在處理任何Timer計時器以前)
kCFRunLoopBeforeSources Inside the event processing loop before any sources are processed.(在處理任何Sources源以前)
kCFRunLoopBeforeWaiting Inside the event processing loop before the run loop sleeps, waiting for a source or timer to fire. This activity does not occur if CFRunLoopRunInMode is called with a timeout of 0 seconds. It also does not occur in a particular iteration of the event processing loop if a version 0 source fires.(在等待源Source和計時器Timer以前)
kCFRunLoopAfterWaiting Inside the event processing loop after the run loop wakes up, but before processing the event that woke it up. This activity occurs only if the run loop did in fact go to sleep during the current loop.(在等待源Source和計時器Timer後,同時在被喚醒以前。)
kCFRunLoopExit The exit of the run loop, after exiting the event processing loop. This activity occurs once for each call to CFRunLoopRun and CFRunLoopRunInMode.(runloop的出口)
kCFRunLoopAllActivities runloop的全部狀態。

PS:想了解更多的runloop信息,可查看以前彬哥寫的博客oop

2、iOS如何監控線程卡頓?

說一下QiLagMonitor中的大體實現思路。性能

  • 首先,建立一個觀察者runLoopObserver,用於觀察主線程的runloop狀態。 同時,還要建立一個信號量dispatchSemaphore,用於保證同步操做。學習

  • 其次,將觀察者runLoopObserver添加到主線程runloop中觀察。

  • 而後,開啓一個子線程,而且在子線程中開啓一個持續的loop來監控主線程runloop的狀態。

  • 若是發現主線程runloop的狀態卡在爲BeforeSources或者AfterWaiting超過88毫秒時,即代表主線程當前卡頓。 這時候,咱們保存主線程當前的調用堆棧便可達成監控目的。

圖解原理:

  • 正常狀況下:

  • 異常狀況下:

3、QiLagMonitor中的具體實現

  • 第一步,建立一個信號量dispatchSemaphore和觀察者runLoopObserver
//! 建立一個信號量,保證同步操做
    dispatchSemaphore = dispatch_semaphore_create(0); //! Dispatch Semaphore保證同步
    //! 建立一個觀察者
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                              kCFRunLoopAllActivities,
                                              YES,
                                              0,
                                              &runLoopObserverCallBack,
                                              &context);
複製代碼

同時當主線程的runloop狀態發生改變時,會調用runLoopObserverCallBack方法,它內部會存儲當前的runloop狀態。同時,控制信號量。

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    QiLagMonitor *lagMonitor = (__bridge QiLagMonitor*)info;
    lagMonitor->runLoopActivity = activity;
    
    dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
    dispatch_semaphore_signal(semaphore);
}
複製代碼
  • 第二步,將觀察者添加到主線程runloopcommon模式下觀察。
//! 將觀察者添加到主線程runloop的common模式下的觀察中
    CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
複製代碼
  • 第三步,建立一個子線程,並開啓一個持續的loop(其實就是個while死循環)來監控主線程的runloop狀態。當runloop的狀態持續爲BeforeSourcesAfterWaiting兩個狀態時,說明主線程卡頓,記錄當前主線程調用堆棧。
//! 建立子線程監控
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //! 子線程開啓一個持續的loop用來進行監控
        while (YES) {
            long semaphoreWait = dispatch_semaphore_wait(self->dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, STUCKMONITORRATE * NSEC_PER_MSEC));
            if (semaphoreWait != 0) {
                if (!self->runLoopObserver) {
                    self->timeoutCount = 0;
                    self->dispatchSemaphore = 0;
                    self->runLoopActivity = 0;
                    return;
                }
                //! 兩個runloop的狀態,BeforeSources和AfterWaiting這兩個狀態區間時間可以檢測到是否卡頓
                if (self->runLoopActivity == kCFRunLoopBeforeSources || self->runLoopActivity == kCFRunLoopAfterWaiting) {
                    //! 出現三次出結果
                    if (++self->timeoutCount < 3) {
                        continue;
                    }
                    NSLog(@"monitor trigger");
                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                        NSString *stackStr = [QiCallStack callStackWithType:QiCallStackTypeMain];
                        QiCallStackModel *model = [[QiCallStackModel alloc] init];
                        model.stackStr = stackStr;
                        model.isStuck = YES;
                        [[[QiLagDB shareInstance] increaseWithStackModel:model] subscribeNext:^(id x) {}];
                    });
                } // end activity
            }// end semaphore wait
            self->timeoutCount = 0;
        }// end while
    });
複製代碼

最後,本系列我是站在iOS業界巨人的肩膀上完成的,感謝戴銘老師精彩的技術分享。 祝你們學有所成,工做順利。
另附上,戴銘老師課程連接:《iOS開發高手課》,謝謝!

相關文章
相關標籤/搜索