你們好,第一次在掘金這個平臺寫東西。若有錯誤,但願指出。
最近發現網上常常被人討論的APP在線上狀態如何檢測到主線程的卡頓狀況,我也稍微瞭解了一下,前段時間就在一個博主的文章裏看到一篇有部分講解這個問題的,聽說美團用的也是這種方案,具體不得而知,而後我發現網上關於這種問題的實現方案都十分相似,若是屏幕前的你尚未意識過這個問題,那就請聽我往下分析這個網上經常使用的檢測方案:bash
利用runloop的檢測方案異步
關於runloop是什麼我就很少說了,由於網上有不少關於這個的文章,最推薦的仍是YYKit的做者博客上那篇。
我要拿出來注意的是 runloop 的狀態:async
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
};複製代碼
網上熱議的是利用 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 這兩個狀態之間的耗時進行判斷是否有太多事件處理致使出現了卡頓,下面直接上代碼:函數
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
PingConfig *object = (__bridge PingConfig*)info;
// 記錄狀態值
object->activity = activity;
// 發送信號
dispatch_semaphore_t semaphore = object->semaphore;
dispatch_semaphore_signal(semaphore);
}複製代碼
上面這些是監聽runloop的狀態而寫的回調函數oop
- (void)registerObserver
{
PingConfig *config = [PingConfig new];
// 建立信號
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
config->semaphore = semaphore;
CFRunLoopObserverContext context = {0,(__bridge void*)config,NULL,NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
__block uint8_t timeoutCount = 0;
// 在子線程監控時長
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
// 假定連續5次超時50ms認爲卡頓(固然也包含了單次超時250ms)
long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if (st != 0)
{
// NSLog(@"循環中--%ld",config->activity);
if (config->activity==kCFRunLoopBeforeSources || config->activity==kCFRunLoopAfterWaiting)
{
if (++timeoutCount < 5){
continue;
}else{
NSLog(@"卡頓了");
}
}
}
timeoutCount = 0;
}
});
}複製代碼
如今我解讀一下這段代碼:測試
@interface PingConfig : NSObject
{
@public
CFRunLoopActivity activity;
dispatch_semaphore_t semaphore;
}
@end複製代碼
恩,只有這麼多足矣。通過測試,的確能夠檢測到主線程的卡頓現象,不得不佩服大佬們的方案。
可是在一次測試中,發現當主線程卡在界面還沒有徹底顯示前,這個方案就檢測不出來卡頓了,好比我將下面的代碼放在B控制器中:優化
dispatch_semaphore_t t = dispatch_semaphore_create(0);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"----");
dispatch_semaphore_signal(t);
});
dispatch_semaphore_wait(t, DISPATCH_TIME_FOREVER);複製代碼
上面是一段有問題的代碼,將致使主線程的持續堵塞,若是咱們在這段代碼放在B控制器的ViewDidLoad方法中(ViewWillAppear一樣),這樣運行後,當你但願push到B控制器時,項目將在上一個界面徹底卡住,而且沒法用上面的方案檢測到,並且CPU及內存都顯示正常:ui
具體緣由我想了一下,因爲runloop在處理完source0或者source1後,好比界面的跳轉也是執行了方法,具體有沒有用到source0這不重要,可是後面會緊接着進入準備睡眠(kCFRunLoopBeforeWaiting)的狀態,然而此時線程的阻塞致使runloop的狀態也被卡住沒法切換,這樣也就致使在那段檢測代碼中沒法進入條件,從而檢測不出來。
可是話說回來,APP在靜止狀態(保持休眠)和剛剛那種卡死狀態都會使runloop維持在 kCFRunLoopBeforeWaiting狀態,這樣咱們就沒法在那段代碼中增長判斷來修復,由於沒法知道究竟是真的靜止沒有操做仍是被阻塞住,我也沒找到線程的阻塞狀態屬性,若是你發現這個屬性,那麼就可使用那個屬性來判斷。可是我也得說下在沒找到那個屬性時個人檢測方案:spa
個人檢測方案線程
先上代碼:
dispatch_queue_t serialQueue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, serialQueue);
dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, 0.25 * NSEC_PER_SEC, 0);
__block int8_t chokeCount = 0;
dispatch_semaphore_t t2 = dispatch_semaphore_create(0);
dispatch_source_set_event_handler(self.timer, ^{
if (config->activity == kCFRunLoopBeforeWaiting) {
static BOOL ex = YES;
if (ex == NO) {
chokeCount ++;
if (chokeCount > 40) {
NSLog(@"差很少卡死了");
dispatch_suspend(self.timer);
return ;
}
NSLog(@"卡頓了");
return ;
}
dispatch_async(dispatch_get_main_queue(), ^{
ex = YES;
dispatch_semaphore_signal(t2);
});
BOOL su = dispatch_semaphore_wait(t2, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if (su != 0) {
ex = NO;
};
}
});
dispatch_resume(self.timer);複製代碼
解釋一下個人方案:
我寫的這段解決方案中的示例代碼只是用來演示,具體是原理能夠你們盡情在此基礎上優化,目前在個人項目中能夠正常檢測到以前那種阻塞形成的APP卡死現象,若是你發現有更好的檢測方案,但願能告訴我,謝謝!