關於線上檢測主線程卡頓的問題

你們好,第一次在掘金這個平臺寫東西。若有錯誤,但願指出。
最近發現網上常常被人討論的APP在線上狀態如何檢測到主線程的卡頓狀況,我也稍微瞭解了一下,前段時間就在一個博主的文章裏看到一篇有部分講解這個問題的,聽說美團用的也是這種方案,具體不得而知,而後我發現網上關於這種問題的實現方案都十分相似,若是屏幕前的你尚未意識過這個問題,那就請聽我往下分析這個網上經常使用的檢測方案:bash

利用runloop的檢測方案異步

關於runloop是什麼我就很少說了,由於網上有不少關於這個的文章,最推薦的仍是YYKit的做者博客上那篇。
我要拿出來注意的是 runloop 的狀態:async

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即將進入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即將處理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 剛從休眠中喚醒
    kCFRunLoopExit          = (1UL << 7), // 即將退出Loop
};複製代碼

網上熱議的是利用 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 這兩個狀態之間的耗時進行判斷是否有太多事件處理致使出現了卡頓,下面直接上代碼:函數

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    PingConfig *object = (__bridge PingConfig*)info;

    // 記錄狀態值
    object->activity = activity;

    // 發送信號
    dispatch_semaphore_t semaphore = object->semaphore;
    dispatch_semaphore_signal(semaphore);
}複製代碼

上面這些是監聽runloop的狀態而寫的回調函數oop

- (void)registerObserver
{
    PingConfig *config = [PingConfig new];
    // 建立信號
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    config->semaphore = semaphore;

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

    __block uint8_t timeoutCount = 0;

    // 在子線程監控時長
    dispatch_async(dispatch_get_global_queue(0, 0), ^{


        while (YES)
        {
            // 假定連續5次超時50ms認爲卡頓(固然也包含了單次超時250ms)
            long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));

            if (st != 0)
            {

//                NSLog(@"循環中--%ld",config->activity);
                if (config->activity==kCFRunLoopBeforeSources || config->activity==kCFRunLoopAfterWaiting)
                {
                    if (++timeoutCount < 5){
                        continue;
                    }else{
                        NSLog(@"卡頓了");
                    }

                }


            }
            timeoutCount = 0;
        }
    });
}複製代碼

如今我解讀一下這段代碼:測試

  1. PingConfig 只是我隨便寫的一個用來存儲runloop的狀態和信號量的自定義類,其中的結構以下:
    @interface PingConfig : NSObject
    {
     @public
     CFRunLoopActivity activity;
     dispatch_semaphore_t semaphore;
    }
    @end複製代碼
    恩,只有這麼多足矣。
  2. APP啓動時我能夠進入 registerObserver 方法,其中首先我建立一個記錄信息的類PingConfig實例,而後建立一個信號,而且保存在這個PingConfig實例中(其實只是爲了方便拿到)。
  3. 接下來我建立了一個觀察者監測主線程的 runloop,它會在主線程runloop狀態切換時進行回調。
  4. 開啓一個子線程,而且在裏面進行一個 while 循環,在 循環的開始處 wait 一個信號量,而且設置超時爲 50毫秒,失敗後會返回一個非0數,成功將會返回0,這時候線程會阻塞住等待一個信號的發出。
  5. 若是runloop狀態正常切換,那麼就會進入回調函數,在回調函數中咱們發出一個信號,而且記錄當前狀態到PingConfig實例中,下面的判斷語句中發現爲0,timeoutCount自動置爲0,一切正常。
  6. 當主線程出現卡頓,while循環中的信號量再次等待,可是回調函數沒有觸發,從而致使等待超時,返回一個非0數,進入判斷句後,咱們再次判斷狀態是否處於 kCFRunLoopBeforeSources 或 kCFRunLoopAfterWaiting,若是成立,timeoutCount+1。
  7. 持續五次runloop不切換狀態,說明runloop正在處理某個棘手的事件沒法休息且不更新狀態,這樣while循環中的信號量超時會一直髮生,超過五次後咱們將判定主線程的卡頓並上傳堆棧信息。

通過測試,的確能夠檢測到主線程的卡頓現象,不得不佩服大佬們的方案。
可是在一次測試中,發現當主線程卡在界面還沒有徹底顯示前,這個方案就檢測不出來卡頓了,好比我將下面的代碼放在B控制器中:優化

dispatch_semaphore_t t = dispatch_semaphore_create(0);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"----");
        dispatch_semaphore_signal(t);
    });
    dispatch_semaphore_wait(t, DISPATCH_TIME_FOREVER);複製代碼

上面是一段有問題的代碼,將致使主線程的持續堵塞,若是咱們在這段代碼放在B控制器的ViewDidLoad方法中(ViewWillAppear一樣),這樣運行後,當你但願push到B控制器時,項目將在上一個界面徹底卡住,而且沒法用上面的方案檢測到,並且CPU及內存都顯示正常:ui

QQ20170930-153549@2x.png
QQ20170930-153549@2x.png

具體緣由我想了一下,因爲runloop在處理完source0或者source1後,好比界面的跳轉也是執行了方法,具體有沒有用到source0這不重要,可是後面會緊接着進入準備睡眠(kCFRunLoopBeforeWaiting)的狀態,然而此時線程的阻塞致使runloop的狀態也被卡住沒法切換,這樣也就致使在那段檢測代碼中沒法進入條件,從而檢測不出來。
可是話說回來,APP在靜止狀態(保持休眠)和剛剛那種卡死狀態都會使runloop維持在 kCFRunLoopBeforeWaiting狀態,這樣咱們就沒法在那段代碼中增長判斷來修復,由於沒法知道究竟是真的靜止沒有操做仍是被阻塞住,我也沒找到線程的阻塞狀態屬性,若是你發現這個屬性,那麼就可使用那個屬性來判斷。可是我也得說下在沒找到那個屬性時個人檢測方案:spa

個人檢測方案線程

先上代碼:

dispatch_queue_t serialQueue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, serialQueue);
    dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, 0.25 * NSEC_PER_SEC, 0);

    __block int8_t chokeCount = 0;
    dispatch_semaphore_t t2 = dispatch_semaphore_create(0);
    dispatch_source_set_event_handler(self.timer, ^{
        if (config->activity == kCFRunLoopBeforeWaiting) {
            static BOOL ex = YES;
            if (ex == NO) {
                chokeCount ++;
                if (chokeCount > 40) {
                    NSLog(@"差很少卡死了");
                    dispatch_suspend(self.timer);
                    return ;
                }
                NSLog(@"卡頓了");
                return ;
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                ex = YES;
                dispatch_semaphore_signal(t2);
            });
            BOOL su = dispatch_semaphore_wait(t2, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
            if (su != 0) {
                ex = NO;
            };
        }
    });
    dispatch_resume(self.timer);複製代碼

解釋一下個人方案:

  1. 開啓一個異步隊列,而且建立一個定時器,時間我設置的是0.25秒,具體時間隨你本身,這個時間是用來檢測卡死的持續時間。
  2. 在定時器外面我也一樣建立了一個用來同步的信號量,這個不解釋了,不會的就去看一下信號量的使用方式。進入定時器的回調後,我設置了一個靜態變量來記錄主隊列是否執行完成。
  3. 咱們判斷當前runloop的狀態是否爲kCFRunLoopBeforeWaiting,因此這個方案是用來彌補前面那個方案,若是主線程此時沒有阻塞住,咱們在這裏向main Queue拋一個block,看它是否可以成功執行,若是成功執行,說明主線程沒有阻塞住,若是已經被阻塞住,那我拋過去的block是確定不會被執行的。
  4. 下面的代碼就是一些輔助操做,當信號量超過50毫秒,拋給主線程的block沒有執行,那麼說明此時就有一些阻塞了,返回一個非0數,並設置 ex爲NO,從而在下一次定時器回調到來時進行上報。

我寫的這段解決方案中的示例代碼只是用來演示,具體是原理能夠你們盡情在此基礎上優化,目前在個人項目中能夠正常檢測到以前那種阻塞形成的APP卡死現象,若是你發現有更好的檢測方案,但願能告訴我,謝謝!

相關文章
相關標籤/搜索