Objective-C 之 利用RunLoop監測卡頓

最近學習戴銘大神的課程,其中一篇文章介紹瞭如何利用RunLoop監測卡頓,在此作個記錄。git

1、監測卡頓的原理

文中介紹到:github

RunLoop是用來監聽輸入源,進行調度處理的。若是RunLoop的線程進入睡眠前方法的執行時間過長而致使沒法進入睡眠,或者線程喚醒後接收消息時間過長而沒法進入下一步,就能夠認爲是線程受阻了。若是這個線程是主線程的話,表現出來的就是出現了卡頓。bash

RunLoop有如下幾種狀態:服務器

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry , // 即將進入 loop,值是2^0
    kCFRunLoopBeforeTimers , // 即將處理 Timer ,值是2^1
    kCFRunLoopBeforeSources , // 即將處理 Source0 ,值是2^2
    kCFRunLoopBeforeWaiting , // 即將進入睡眠,等待 mach_port 消息,值是2^5
    kCFRunLoopAfterWaiting , // 即將處理 mach_port 消息,值是2^6
    kCFRunLoopExit , // 即將退出 loop,值是2^7
    kCFRunLoopAllActivities  // loop 全部狀態改變。經過監測該值的變化,就知道runLoop的狀態發生了變化
}
複製代碼

而文中提到的2種線程受阻狀況,它們的狀態分別是:kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting。app

2、思路

根據原理,能夠獲得一個監測卡頓的思路:async

監測主線程RunLoop的狀態,若是狀態在必定時長內都是kCFRunLoopBeforeSources或者kCFRunLoopAfterWaiting,則認爲卡頓。函數

步驟以下:oop

  1. 建立一個RunLoop的觀察者(CFRunLoopObserverRef)
  2. 把觀察者加入主線程的kCFRunLoopCommonModes模式中,以監測主線程
  3. 建立一個子線程來維護觀察者
  4. 根據主線程RunLoop的狀態來判斷是否卡頓

3、實現方法

1. 實現代碼

// 在AppDelaget.m文件添加幾個屬性
@interface AppDelegate ()
{
    int timeoutCount;
    CFRunLoopObserverRef runLoopObserver;
@public
    dispatch_semaphore_t dispatchSemaphore;
    CFRunLoopActivity runLoopActivity;
}
@end

@implementation AppDelegate

// 開始監測
- (void)beginMonitor {

    if (runLoopObserver) {
        return;
    }
    
    // dispatchSemaphore的知識參考:https://www.jianshu.com/p/24ffa819379c
    // 初始化信號量,值爲0
    dispatchSemaphore = dispatch_semaphore_create(0); //Dispatch Semaphore保證同步
    
    // 建立一個觀察者
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                              kCFRunLoopAllActivities,
                                              YES,
                                              0,
                                              &runLoopObserverCallBack,
                                              &context);
    // 將觀察者添加到主線程runloop的commonModes中
    CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
    
    // 建立子線程監控
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 子線程開啓一個持續的loop用來進行監控
        while (1) {
            // 等待信號量:若是信號量是0,則阻塞當前線程;若是信號量大於0,則此函數會把信號量-1,繼續執行線程。此處超時時間設爲20毫秒。
            // 返回值:若是線程是喚醒的,則返回非0,不然返回0
            long semaphoreWait = dispatch_semaphore_wait(self->dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 20*NSEC_PER_MSEC));
//            NSLog(@"%@",@(semaphoreWait));
            if (semaphoreWait != 0) {
                if (!self->runLoopObserver) {// observer建立失敗,直接返回
                    self->timeoutCount = 0;
                    self->dispatchSemaphore = 0;
                    self->runLoopActivity = 0;
                    return;
                }
                
                // 若是RunLoop執行任務的時間過長(kCFRunLoopBeforeSources),或者線程喚醒後接收消息時間過長(kCFRunLoopAfterWaiting),則認爲線程受阻。
                if (self->runLoopActivity == kCFRunLoopBeforeSources || self->runLoopActivity == kCFRunLoopAfterWaiting) {
                    NSLog(@"runloop狀態:%@",@(self->runLoopActivity));
                    // 60毫秒內一直保持其中一種狀態,說明卡頓(20毫秒測1次,共3次)
                    if (++self->timeoutCount < 3) {
                        continue;
                    }
                    NSLog(@"發生卡頓...");
                }
            }
            self->timeoutCount = 0;
        }
    });
    
}

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
//    NSLog(@"監測到線程主線程有變化,信號量+1");
    AppDelegate *delegate = (__bridge AppDelegate*)info;
    delegate->runLoopActivity = activity;
    
    dispatch_semaphore_t semaphore = delegate->dispatchSemaphore;
    // 讓信號量+1
    dispatch_semaphore_signal(semaphore);
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 開始監測
    [self beginMonitor];
    
    return YES;
}

@end

複製代碼

監測到卡頓後,使用PLCrashRepoter獲取堆棧信息,根據這些信息找到形成卡頓的方法。學習

#import <CrashReporter/CrashReporter.h>

// 獲取數據
NSData *lagData = [[[PLCrashReporter alloc]
                    initWithConfiguration:[[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll]] generateLiveReport];
// 轉換成 PLCrashReport 對象
PLCrashReport *lagReport = [[PLCrashReport alloc] initWithData:lagData error:NULL];
// 進行字符串格式化處理
NSString *lagReportString = [PLCrashReportTextFormatter stringValueForCrashReport:lagReport withTextFormat:PLCrashReportTextFormatiOS];
// 將字符串上傳服務器
NSLog(@"lag happen, detail below: \n %@",lagReportString);
複製代碼

2. 流程說明:

爲了保證子線程的同步監測,剛開始建立一個信號量是0的dispatch_semaphore。當監測到主線程的RunLoop的狀態發生變化,觸發回調:測試

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
複製代碼

在回調裏面發送信號,使信號量+1,值變爲1:

dispatch_semaphore_signal(semaphore)
複製代碼

dispatch_semaphore_wait接收到信號量不爲0,會返回一個不爲0的值(semaphoreWait),並把信號量減1:

long semaphoreWait = dispatch_semaphore_wait(self->dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 20*NSEC_PER_MSEC));
複製代碼

而後觸發一次監測功能,記錄RunLoop狀態。此時信號量是0,繼續等待下一個信號量。

反覆監測後,若是狀態連續3次都是kCFRunLoopBeforeSources或者kCFRunLoopAfterWaiting,則斷定爲卡頓。

3. 關於觸發卡頓的時間閾值

代碼中定義了卡頓閾值是3*20ms=60ms,在線下作測試的話,這個值是合理的。可是若是在線上使用這種監測卡頓的方法,大神建議把該值設爲3秒,文中介紹以下:

其實,觸發卡頓的時間閾值,咱們能夠根據 WatchDog 機制來設置。WatchDog 在不一樣狀態下設置的不一樣時間,以下所示:

啓動(Launch):20s; 恢復(Resume):10s; 掛起(Suspend):10s; 退出(Quit):6s; 後臺(Background):3min(在 iOS 7 以前,每次申請 10min; 以後改成每次申請 3min,可連續申請,最多申請到 10min)。

經過 WatchDog 設置的時間,我認爲能夠把啓動的閾值設置爲 10 秒,其餘狀態則都默認設置爲 3 秒。總的原則就是,要小於 WatchDog 的限制時間。固然了,這個閾值也不用小得太多,原則就是要優先解決用戶感知最明顯的體驗問題。

參考項目:github.com/ming1016/De…

相關文章
相關標籤/搜索