最近學習戴銘大神的課程,其中一篇文章介紹瞭如何利用RunLoop監測卡頓,在此作個記錄。git
文中介紹到: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
根據原理,能夠獲得一個監測卡頓的思路:async
監測主線程RunLoop的狀態,若是狀態在必定時長內都是kCFRunLoopBeforeSources
或者kCFRunLoopAfterWaiting
,則認爲卡頓。函數
步驟以下:oop
// 在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);
複製代碼
爲了保證子線程的同步監測,剛開始建立一個信號量是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*20ms=60ms,在線下作測試的話,這個值是合理的。可是若是在線上使用這種監測卡頓的方法,大神建議把該值設爲3秒
,文中介紹以下:
其實,觸發卡頓的時間閾值,咱們能夠根據 WatchDog 機制來設置。WatchDog 在不一樣狀態下設置的不一樣時間,以下所示:
啓動(Launch):20s; 恢復(Resume):10s; 掛起(Suspend):10s; 退出(Quit):6s; 後臺(Background):3min(在 iOS 7 以前,每次申請 10min; 以後改成每次申請 3min,可連續申請,最多申請到 10min)。
經過 WatchDog 設置的時間,我認爲能夠把啓動的閾值設置爲 10 秒,其餘狀態則都默認設置爲 3 秒。總的原則就是,要小於 WatchDog 的限制時間。固然了,這個閾值也不用小得太多,原則就是要優先解決用戶感知最明顯的體驗問題。