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

級別:★★☆☆☆
標簽:「iOS」「性能監(jiān)控」「工具」「RunLoop」
作者: 647
審校: QiShare團隊


前言:
最近,在看戴銘老師關(guān)于 “性能監(jiān)控” 相關(guān)的技術(shù)分享,感覺收獲很多。基于最近的學(xué)習(xí),總結(jié)了一些性能監(jiān)控相關(guān)的實踐,并計劃落地一系列 “性能監(jiān)控” 相關(guān)的文章。

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


本篇將介紹iOS性能監(jiān)控工具(QiLagMonitor)中與 “線程卡頓監(jiān)控” 相關(guān)的功能模塊。

一、了解線程的狀態(tài)

主線程runloop默認注冊了五個modekCFRunLoopDefaultModeUITrackingRunLoopModeUIInitializationRunLoopModeGSEventReceiveRunLoopModekCFRunLoopCommonModes

名稱 作用
kCFRunLoopDefaultMode App的默認 Mode,通常主線程是在這個 Mode 下運行的。
UITrackingRunLoopMode 界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響。
UIInitializationRunLoopMode 剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用。
GSEventReceiveRunLoopMode 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到。
kCFRunLoopCommonModes 這是一個占位的 Mode。其實就是Default模式和UI模式之間切換使用。

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

而我們的下節(jié)要介紹的主線程監(jiān)控就是使用的就是NSRunLoopCommonModeskCFRunLoopCommonModes)。

然后,runloop觀察者:Runloop Observer7種狀態(tài)。

/* 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
};

分別表示:

狀態(tài) 含義
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的所有狀態(tài)。

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

二、iOS如何監(jiān)控線程卡頓?

說一下QiLagMonitor中的大致實現(xiàn)思路。

  • 首先,創(chuàng)建一個觀察者runLoopObserver,用于觀察主線程的runloop狀態(tài)。
    同時,還要創(chuàng)建一個信號量dispatchSemaphore,用于保證同步操作。

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

  • 然后,開啟一個子線程,并且在子線程中開啟一個持續(xù)的loop來監(jiān)控主線程runloop的狀態(tài)。

  • 如果發(fā)現(xiàn)主線程runloop的狀態(tài)卡在為BeforeSources或者AfterWaiting超過88毫秒時,即表明主線程當前卡頓。
    這時候,我們保存主線程當前的調(diào)用堆棧即可達成監(jiān)控目的。

圖解原理:

  • 正常情況下:
  • 異常情況下:

三、QiLagMonitor中的具體實現(xiàn)

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

同時當主線程的runloop狀態(tài)發(fā)生改變時,會調(diào)用runLoopObserverCallBack方法,它內(nèi)部會存儲當前的runloop狀態(tài)。同時,控制信號量。

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);
  • 第三步,創(chuàng)建一個子線程,并開啟一個持續(xù)的loop(其實就是個while死循環(huán))來監(jiān)控主線程的runloop狀態(tài)。當runloop的狀態(tài)持續(xù)為BeforeSourcesAfterWaiting兩個狀態(tài)時,說明主線程卡頓,記錄當前主線程調(diào)用堆棧。
    //! 創(chuàng)建子線程監(jiān)控
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //! 子線程開啟一個持續(xù)的loop用來進行監(jiān)控
        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的狀態(tài),BeforeSources和AfterWaiting這兩個狀態(tài)區(qū)間時間能夠檢測到是否卡頓
                if (self->runLoopActivity == kCFRunLoopBeforeSources || self->runLoopActivity == kCFRunLoopAfterWaiting) {
                    //! 出現(xiàn)三次出結(jié)果
                    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
    });

推薦文章:
元旦福利!QiShare給大家發(fā)2020新年紅包啦~
iOS 性能監(jiān)控(一)—— CPU功耗監(jiān)控
初識Flutter web
用SwiftUI給視圖添加動畫
用SwiftUI寫一個簡單頁面
iOS App啟動優(yōu)化(三)—— 自己做一個工具監(jiān)控App的啟動耗時
iOS App啟動優(yōu)化(二)—— 使用“Time Profiler”工具監(jiān)控App的啟動耗時
iOS App啟動優(yōu)化(一)—— 了解App的啟動流程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內(nèi)容