RunLoop實戰:實時卡頓監控

minjing_lin發表於2019-04-09

RunLoop實戰:實時卡頓監控

上篇文章說道,RunLoop總結與面試,搞懂了RunLoop底層原理,當然要寫東西練手嘍,參考之前同事寫的工具和一些文章,輸出此文。

1.尋找卡頓切入點

監控卡頓,說白了就是找到主執行緒都在幹些啥。 我們知道一個執行緒的訊息事件處理都是依賴於NSRunLoop來驅動,所以要知道執行緒正在呼叫什麼方法,就需要從NSRunLoop來入手。

RunLoop的執行程式碼大致如下:

{
    /// 1. 通知Observers,即將進入RunLoop
    /// 此處有Observer會建立AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {
 
        /// 2. 通知 Observers: 即將觸發 Timer 回撥。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        /// 3. 通知 Observers: 即將觸發 Source (非基於port的,Source0) 回撥。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 4. 觸發 Source0 (非基於port的) 回撥。
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 6. 通知Observers,即將進入休眠
        /// 此處有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,執行緒被喚醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
 
        /// 9. 如果是被Timer喚醒的,回撥Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
 
        /// 9. 如果是被dispatch喚醒的,執行所有呼叫 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
 
        /// 9. 如果如果Runloop是被 Source1 (基於port的) 的事件喚醒了,處理這個事件
        __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處理事件的時間主要出在兩個階段:

  • kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之間
  • kCFRunLoopAfterWaiting之後

2.RunLoop 函式

我們可以使用CFRunLoopObserverRef來監控NSRunLoop的狀態,通過它可以實時獲得這些狀態值的變化。

  1. 設定Runloop observer的執行環境
    CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};

  2. 建立Runloop observer物件
    第一個引數:用於分配observer物件的記憶體 第二個引數:用以設定observer所要關注的事件,詳見回撥函式myRunLoopObserver中註釋 第三個引數:用於標識該observer是在第一次進入runloop時執行還是每次進入runloop處理時均執行 第四個引數:用於設定該observer的優先順序 第五個引數:用於設定該observer的回撥函式 第六個引數:用於設定該observer的執行環境
    CFRunLoopObserverCreate(<#CFAllocatorRef allocator#>, <#CFOptionFlags activities#>, <#Boolean repeats#>, <#CFIndex order#>, <#CFRunLoopObserverCallBack callout#>, <#CFRunLoopObserverContext *context#>)

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

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

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

3.訊號量

//建立訊號量,引數:訊號量的初值,如果小於0則會返回NULL
dispatch_semaphore_create(訊號量值)
 
//等待降低訊號量
dispatch_semaphore_wait(訊號量,等待時間)
 
//提高訊號量
dispatch_semaphore_signal(訊號量)
複製程式碼

注意:正常的使用順序是先降低然後再提高,這兩個函式通常成對使用。

4.量化卡頓的程度

原理: 利用觀察Runloop各種狀態變化的持續時間來檢測計算是否發生卡頓 一次有效卡頓採用了“N次卡頓超過閾值T”的判定策略,即一個時間段內卡頓的次數累計大於N時才觸發採集和上報:舉例,卡頓閾值T=500ms、卡頓次數N=1,可以判定為單次耗時較長的一次有效卡頓;而卡頓閾值T=50ms、卡頓次數N=5,可以判定為頻次較快的一次有效卡頓

實踐: 我們需要開啟一個子執行緒,實時計算兩個狀態區域之間的耗時是否到達某個閥值。另外卡頓需要覆蓋到多次連續小卡頓和單次長時間卡頓兩種情景。

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    MJMonitorRunloop *instance = [MJMonitorRunloop sharedInstance];
    // 記錄狀態值
    instance->_activity = activity;
    // 傳送訊號
    dispatch_semaphore_t semaphore = instance->_semaphore;
    dispatch_semaphore_signal(semaphore);
}

// 註冊一個Observer來監測Loop的狀態,回撥函式是runLoopObserverCallBack
- (void)registerObserver
{
    // 設定Runloop observer的執行環境
    CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
    // 建立Runloop observer物件
    _observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                        kCFRunLoopAllActivities,
                                        YES,
                                        0,
                                        &runLoopObserverCallBack,
                                        &context);
    // 將新建的observer加入到當前thread的runloop
    CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
    // 建立訊號
    _semaphore = dispatch_semaphore_create(0);
    
    __weak __typeof(self) weakSelf = self;
    // 在子執行緒監控時長
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        __strong __typeof(weakSelf) strongSelf = weakSelf;
        if (!strongSelf) {
            return;
        }
        while (YES) {
            if (strongSelf.isCancel) {
                return;
            }
            // N次卡頓超過閾值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.測試用例

用一個tableView檢視,上下拖動,人為設定卡頓(休眠),來測試我們實時監控困頓的程式碼是否有效。

- (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.記錄卡頓資料

當檢測到卡頓時,抓取堆疊資訊,然後在客戶端做一些過濾處理,(Debug)可以儲存在本地,(Release)可以上傳伺服器,通過收集一定量的卡頓資料後,經過分析便能準確定位需要優化的地方。

堆疊資訊

獲取堆疊資訊後,可以使用Demo中MJCallStack類(參考:BSBacktraceLogger—輕量級呼叫棧分析器) 或 KSCrash、PLCrashReporter等來解析。

函式資訊

至此這個實時卡頓監控就大功告成了。
GitHub地址: MJRunLoopDemo

參考文章:

簡單監測iOS卡頓的demo
iOS實時卡頓監控
BSBacktraceLogger
RunLoop總結與面試
dispatch_semaphore(訊號量)的理解及使用

相關文章