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

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

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


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

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

主線程runloop默認(rèn)注冊了五個(gè)modekCFRunLoopDefaultModeUITrackingRunLoopMode戴甩、UIInitializationRunLoopMode符喝、GSEventReceiveRunLoopModekCFRunLoopCommonModes甜孤。

名稱 作用
kCFRunLoopDefaultMode App的默認(rèn) Mode协饲,通常主線程是在這個(gè) Mode 下運(yùn)行的畏腕。
UITrackingRunLoopMode 界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動(dòng)茉稠,保證界面滑動(dòng)時(shí)不受其他 Mode 影響描馅。
UIInitializationRunLoopMode 剛啟動(dòng) App 時(shí)第進(jìn)入的第一個(gè) Mode,啟動(dòng)完成后就不再使用而线。
GSEventReceiveRunLoopMode 接受系統(tǒng)事件的內(nèi)部 Mode铭污,通常用不到。
kCFRunLoopCommonModes 這是一個(gè)占位的 Mode膀篮。其實(shí)就是Default模式和UI模式之間切換使用嘹狞。

其中,APPLE公開提供的Mode有兩個(gè):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 運(yùn)行runloop的入口。
kCFRunLoopBeforeTimers Inside the event processing loop before any timers are processed.(在處理任何Timer計(jì)時(shí)器之前)
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和計(jì)時(shí)器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和計(jì)時(shí)器Timer后速蕊,同時(shí)在被喚醒之前嫂丙。)
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中的大致實(shí)現(xiàn)思路隅肥。

  • 首先,創(chuàng)建一個(gè)觀察者runLoopObserver袄简,用于觀察主線程的runloop狀態(tài)腥放。
    同時(shí),還要?jiǎng)?chuàng)建一個(gè)信號量dispatchSemaphore绿语,用于保證同步操作秃症。

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

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

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

圖解原理:

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

三炸卑、QiLagMonitor中的具體實(shí)現(xiàn)

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

同時(shí)當(dāng)主線程的runloop狀態(tài)發(fā)生改變時(shí),會(huì)調(diào)用runLoopObserverCallBack方法患久,它內(nèi)部會(huì)存儲(chǔ)當(dāng)前的runloop狀態(tài)椅寺。同時(shí),控制信號量蒋失。

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)建一個(gè)子線程荆萤,并開啟一個(gè)持續(xù)的loop(其實(shí)就是個(gè)while死循環(huán))來監(jiān)控主線程的runloop狀態(tài)。當(dāng)runloop的狀態(tài)持續(xù)為BeforeSources铣卡、AfterWaiting兩個(gè)狀態(tài)時(shí)链韭,說明主線程卡頓,記錄當(dāng)前主線程調(diào)用堆棧煮落。
    //! 創(chuàng)建子線程監(jiān)控
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //! 子線程開啟一個(gè)持續(xù)的loop用來進(jìn)行監(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;
                }
                //! 兩個(gè)runloop的狀態(tài)敞峭,BeforeSources和AfterWaiting這兩個(gè)狀態(tài)區(qū)間時(shí)間能夠檢測到是否卡頓
                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
    });

最后,本系列我是站在iOS業(yè)界巨人的肩膀上完成的蝉仇,感謝戴銘老師精彩的技術(shù)分享旋讹。 祝大家學(xué)有所成,工作順利轿衔。
另附上沉迹,戴銘老師課程鏈接:《iOS開發(fā)高手課》,謝謝害驹!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鞭呕,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子宛官,更是在濱河造成了極大的恐慌葫松,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摘刑,死亡現(xiàn)場離奇詭異进宝,居然都是意外死亡刻坊,警方通過查閱死者的電腦和手機(jī)枷恕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谭胚,“玉大人徐块,你說我怎么就攤上這事未玻。” “怎么了胡控?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵扳剿,是天一觀的道長。 經(jīng)常有香客問我昼激,道長庇绽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任橙困,我火速辦了婚禮瞧掺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘凡傅。我一直安慰自己辟狈,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布夏跷。 她就那樣靜靜地躺著哼转,像睡著了一般。 火紅的嫁衣襯著肌膚如雪槽华。 梳的紋絲不亂的頭發(fā)上壹蔓,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機(jī)與錄音猫态,去河邊找鬼庶溶。 笑死,一個(gè)胖子當(dāng)著我的面吹牛懂鸵,可吹牛的內(nèi)容都是我干的偏螺。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼匆光,長吁一口氣:“原來是場噩夢啊……” “哼套像!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起终息,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤夺巩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后周崭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體柳譬,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年续镇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了美澳。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,064評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖制跟,靈堂內(nèi)的尸體忽然破棺而出舅桩,到底是詐尸還是另有隱情,我是刑警寧澤雨膨,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布擂涛,位于F島的核電站,受9級特大地震影響聊记,放射性物質(zhì)發(fā)生泄漏撒妈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一排监、第九天 我趴在偏房一處隱蔽的房頂上張望踩身。 院中可真熱鬧,春花似錦社露、人聲如沸挟阻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽附鸽。三九已至,卻和暖如春瞒瘸,著一層夾襖步出監(jiān)牢的瞬間坷备,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工情臭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留省撑,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓俯在,卻偏偏與公主長得像竟秫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子跷乐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評論 2 345