RunLoop實(shí)戰(zhàn):實(shí)時(shí)卡頓監(jiān)控

上篇文章說(shuō)道,RunLoop總結(jié)與面試吗货,搞懂了RunLoop底層原理泳唠,當(dāng)然要寫(xiě)東西練手嘍,參考之前同事寫(xiě)的工具和一些文章宙搬,輸出此文笨腥。

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

監(jiān)控卡頓,說(shuō)白了就是找到主線(xiàn)程都在干些啥勇垛。 我們知道一個(gè)線(xiàn)程的消息事件處理都是依賴(lài)于NSRunLoop來(lái)驅(qū)動(dòng),所以要知道線(xiàn)程正在調(diào)用什么方法脖母,就需要從NSRunLoop來(lái)入手。

RunLoop的執(zhí)行代碼大致如下:

{
    /// 1. 通知Observers闲孤,即將進(jìn)入RunLoop
    /// 此處有Observer會(huì)創(chuàng)建AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {
 
        /// 2. 通知 Observers: 即將觸發(fā) Timer 回調(diào)谆级。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        /// 3. 通知 Observers: 即將觸發(fā) Source (非基于port的,Source0) 回調(diào)。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 4. 觸發(fā) Source0 (非基于port的) 回調(diào)讼积。
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 6. 通知Observers肥照,即將進(jìn)入休眠
        /// 此處有Observer釋放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
 
        /// 7. sleep to wait msg.
        mach_msg() -> mach_msg_trap();
 
 
        /// 8. 通知Observers,線(xiàn)程被喚醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
 
        /// 9. 如果是被Timer喚醒的币砂,回調(diào)Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
 
        /// 9. 如果是被dispatch喚醒的建峭,執(zhí)行所有調(diào)用 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
 
        /// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件喚醒了玻侥,處理這個(gè)事件
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
 
 
    } while (...);
 
    /// 10. 通知Observers决摧,即將退出RunLoop
    /// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

從上可以看出RunLoop處理事件的時(shí)間主要出在兩個(gè)階段:

  • kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之間
  • kCFRunLoopAfterWaiting之后

2.RunLoop 函數(shù)

我們可以使用CFRunLoopObserverRef來(lái)監(jiān)控NSRunLoop的狀態(tài),通過(guò)它可以實(shí)時(shí)獲得這些狀態(tài)值的變化。

  1. 設(shè)置Runloop observer的運(yùn)行環(huán)境
    CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};

  2. 創(chuàng)建Runloop observer對(duì)象
    第一個(gè)參數(shù):用于分配observer對(duì)象的內(nèi)存
    第二個(gè)參數(shù):用以設(shè)置observer所要關(guān)注的事件凑兰,詳見(jiàn)回調(diào)函數(shù)myRunLoopObserver中注釋
    第三個(gè)參數(shù):用于標(biāo)識(shí)該observer是在第一次進(jìn)入runloop時(shí)執(zhí)行還是每次進(jìn)入runloop處理時(shí)均執(zhí)行
    第四個(gè)參數(shù):用于設(shè)置該observer的優(yōu)先級(jí)
    第五個(gè)參數(shù):用于設(shè)置該observer的回調(diào)函數(shù)
    第六個(gè)參數(shù):用于設(shè)置該observer的運(yùn)行環(huán)境
    CFRunLoopObserverCreate(<#CFAllocatorRef allocator#>, <#CFOptionFlags activities#>, <#Boolean repeats#>, <#CFIndex order#>, <#CFRunLoopObserverCallBack callout#>, <#CFRunLoopObserverContext *context#>)

  3. 將新建的observer加入到當(dāng)前thread的runloop
    CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);

  4. 將observer從當(dāng)前thread的runloop中移除
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);

  5. 釋放 observer
    CFRelease(_observer); _observer = NULL;

3.信號(hào)量

//創(chuàng)建信號(hào)量掌桩,參數(shù):信號(hào)量的初值,如果小于0則會(huì)返回NULL
dispatch_semaphore_create(信號(hào)量值)
 
//等待降低信號(hào)量
dispatch_semaphore_wait(信號(hào)量姑食,等待時(shí)間)
 
//提高信號(hào)量
dispatch_semaphore_signal(信號(hào)量)

注意:正常的使用順序是先降低然后再提高波岛,這兩個(gè)函數(shù)通常成對(duì)使用融击。

4.量化卡頓的程度

原理:
利用觀(guān)察Runloop各種狀態(tài)變化的持續(xù)時(shí)間來(lái)檢測(cè)計(jì)算是否發(fā)生卡頓
一次有效卡頓采用了“N次卡頓超過(guò)閾值T”的判定策略屑咳,即一個(gè)時(shí)間段內(nèi)卡頓的次數(shù)累計(jì)大于N時(shí)才觸發(fā)采集和上報(bào):舉例,卡頓閾值T=500ms、卡頓次數(shù)N=1券躁,可以判定為單次耗時(shí)較長(zhǎng)的一次有效卡頓;而卡頓閾值T=50ms址否、卡頓次數(shù)N=5贬派,可以判定為頻次較快的一次有效卡頓

實(shí)踐:
我們需要開(kāi)啟一個(gè)子線(xiàn)程,實(shí)時(shí)計(jì)算兩個(gè)狀態(tài)區(qū)域之間的耗時(shí)是否到達(dá)某個(gè)閥值。另外卡頓需要覆蓋到多次連續(xù)小卡頓和單次長(zhǎng)時(shí)間卡頓兩種情景坛善。

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    MJMonitorRunloop *instance = [MJMonitorRunloop sharedInstance];
    // 記錄狀態(tài)值
    instance->_activity = activity;
    // 發(fā)送信號(hào)
    dispatch_semaphore_t semaphore = instance->_semaphore;
    dispatch_semaphore_signal(semaphore);
}

// 注冊(cè)一個(gè)Observer來(lái)監(jiān)測(cè)Loop的狀態(tài),回調(diào)函數(shù)是runLoopObserverCallBack
- (void)registerObserver
{
    // 設(shè)置Runloop observer的運(yùn)行環(huán)境
    CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
    // 創(chuàng)建Runloop observer對(duì)象
    _observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                        kCFRunLoopAllActivities,
                                        YES,
                                        0,
                                        &runLoopObserverCallBack,
                                        &context);
    // 將新建的observer加入到當(dāng)前thread的runloop
    CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
    // 創(chuàng)建信號(hào)
    _semaphore = dispatch_semaphore_create(0);
    
    __weak __typeof(self) weakSelf = self;
    // 在子線(xiàn)程監(jiān)控時(shí)長(zhǎng)
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        __strong __typeof(weakSelf) strongSelf = weakSelf;
        if (!strongSelf) {
            return;
        }
        while (YES) {
            if (strongSelf.isCancel) {
                return;
            }
            // N次卡頓超過(guò)閾值T記錄為一次卡頓
            long dsw = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, strongSelf.limitMillisecond * NSEC_PER_MSEC));
            if (dsw != 0) {
                if (self->_activity == kCFRunLoopBeforeSources || self->_activity == kCFRunLoopAfterWaiting) {
                    if (++strongSelf.countTime < strongSelf.standstillCount){
                        NSLog(@"%ld",strongSelf.countTime);
                        continue;
                    }
                    [strongSelf logStack];
                    [strongSelf printLogTrace];
                    
                    NSString *backtrace = [MJCallStack mj_backtraceOfMainThread];
                    NSLog(@"++++%@",backtrace);
                    
                    if (strongSelf.callbackWhenStandStill) {
                        strongSelf.callbackWhenStandStill();
                    }
                }
            }
            strongSelf.countTime = 0;
        }
    });
}

5.測(cè)試用例

用一個(gè)tableView視圖晾蜘,上下拖動(dòng),人為設(shè)置卡頓(休眠)眠屎,來(lái)測(cè)試我們實(shí)時(shí)監(jiān)控困頓的代碼是否有效剔交。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *identify =@"cellIdentify";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identify];
    if(!cell) {
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identify];
    }
    if (indexPath.row % 10 == 0) {
        usleep(1 * 1000 * 1000); // 1秒
        cell.textLabel.text = @"卡咯";
    }else{
        cell.textLabel.text = [NSString stringWithFormat:@"%ld",indexPath.row];
    }
    
    return cell;
}

6.記錄卡頓數(shù)據(jù)

當(dāng)檢測(cè)到卡頓時(shí),抓取堆棧信息,然后在客戶(hù)端做一些過(guò)濾處理,(Debug)可以保存在本地,(Release)可以上傳服務(wù)器改衩,通過(guò)收集一定量的卡頓數(shù)據(jù)后岖常,經(jīng)過(guò)分析便能準(zhǔn)確定位需要優(yōu)化的地方。

堆棧信息

獲取堆棧信息后葫督,可以使用Demo中MJCallStack類(lèi)(參考:BSBacktraceLogger—輕量級(jí)調(diào)用棧分析器) 或 KSCrash腥椒、PLCrashReporter等來(lái)解析。

函數(shù)信息

至此這個(gè)實(shí)時(shí)卡頓監(jiān)控就大功告成了候衍。GitHub地址:
MJRunLoopDemo

參考文章:

簡(jiǎn)單監(jiān)測(cè)iOS卡頓的demo
iOS實(shí)時(shí)卡頓監(jiān)控
BSBacktraceLogger
RunLoop總結(jié)與面試
dispatch_semaphore(信號(hào)量)的理解及使用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末笼蛛,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蛉鹿,更是在濱河造成了極大的恐慌滨砍,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,816評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件妖异,死亡現(xiàn)場(chǎng)離奇詭異惋戏,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)他膳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)响逢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人棕孙,你說(shuō)我怎么就攤上這事舔亭。” “怎么了蟀俊?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,300評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵钦铺,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我肢预,道長(zhǎng)矛洞,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,780評(píng)論 1 285
  • 正文 為了忘掉前任烫映,我火速辦了婚禮沼本,結(jié)果婚禮上噩峦,老公的妹妹穿的比我還像新娘。我一直安慰自己抽兆,他們只是感情好壕探,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著郊丛,像睡著了一般李请。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上厉熟,一...
    開(kāi)封第一講書(shū)人閱讀 50,084評(píng)論 1 291
  • 那天导盅,我揣著相機(jī)與錄音,去河邊找鬼揍瑟。 笑死白翻,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的绢片。 我是一名探鬼主播滤馍,決...
    沈念sama閱讀 39,151評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼底循!你這毒婦竟也來(lái)了巢株?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,912評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤熙涤,失蹤者是張志新(化名)和其女友劉穎阁苞,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體祠挫,經(jīng)...
    沈念sama閱讀 44,355評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡那槽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了等舔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片骚灸。...
    茶點(diǎn)故事閱讀 38,809評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖慌植,靈堂內(nèi)的尸體忽然破棺而出甚牲,到底是詐尸還是另有隱情,我是刑警寧澤涤浇,帶...
    沈念sama閱讀 34,504評(píng)論 4 334
  • 正文 年R本政府宣布鳖藕,位于F島的核電站,受9級(jí)特大地震影響只锭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜院尔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評(píng)論 3 317
  • 文/蒙蒙 一蜻展、第九天 我趴在偏房一處隱蔽的房頂上張望喉誊。 院中可真熱鬧,春花似錦纵顾、人聲如沸伍茄。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)敷矫。三九已至,卻和暖如春汉额,著一層夾襖步出監(jiān)牢的瞬間曹仗,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,121評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工蠕搜, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留怎茫,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,628評(píng)論 2 362
  • 正文 我出身青樓妓灌,卻偏偏與公主長(zhǎng)得像轨蛤,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子虫埂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評(píng)論 2 351