級別:★★☆☆☆
標簽:「iOS」「性能監(jiān)控」「工具」「RunLoop」
作者: 647
審校: QiShare團隊
前言:
最近,在看戴銘老師關(guān)于 “性能監(jiān)控” 相關(guān)的技術(shù)分享,感覺收獲很多。基于最近的學(xué)習(xí),總結(jié)了一些性能監(jiān)控相關(guān)的實踐,并計劃落地一系列 “性能監(jiān)控” 相關(guān)的文章。
目錄如下:
iOS 性能監(jiān)控(一)—— CPU功耗監(jiān)控
iOS 性能監(jiān)控(二)—— 主線程卡頓監(jiān)控
iOS 性能監(jiān)控(三)—— 方法耗時監(jiān)控
本篇將介紹iOS性能監(jiān)控工具(QiLagMonitor)中與 “線程卡頓監(jiān)控” 相關(guān)的功能模塊。
一、了解線程的狀態(tài)
主線程runloop
默認注冊了五個mode
:kCFRunLoopDefaultMode
、UITrackingRunLoopMode
、UIInitializationRunLoopMode
、GSEventReceiveRunLoopMode
、kCFRunLoopCommonModes
。
名稱 | 作用 |
---|---|
kCFRunLoopDefaultMode | App的默認 Mode,通常主線程是在這個 Mode 下運行的。 |
UITrackingRunLoopMode | 界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響。 |
UIInitializationRunLoopMode | 剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用。 |
GSEventReceiveRunLoopMode | 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到。 |
kCFRunLoopCommonModes | 這是一個占位的 Mode。其實就是Default模式和UI模式之間切換使用。 |
其中,APPLE
公開提供的Mode
有兩個:NSDefaultRunLoopMode
(kCFRunLoopDefaultMode
)、NSRunLoopCommonModes
(kCFRunLoopCommonModes
)。
而我們的下節(jié)要介紹的主線程監(jiān)控就是使用的就是NSRunLoopCommonModes
(kCFRunLoopCommonModes
)。
然后,runloop
觀察者:Runloop Observer
有7
種狀態(tài)。
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
分別表示:
狀態(tài) | 含義 |
---|---|
kCFRunLoopEntry | 運行runloop的入口。 |
kCFRunLoopBeforeTimers | Inside the event processing loop before any timers are processed.(在處理任何Timer 計時器之前) |
kCFRunLoopBeforeSources | Inside the event processing loop before any sources are processed.(在處理任何Sources 源之前) |
kCFRunLoopBeforeWaiting | Inside the event processing loop before the run loop sleeps, waiting for a source or timer to fire. This activity does not occur if CFRunLoopRunInMode is called with a timeout of 0 seconds. It also does not occur in a particular iteration of the event processing loop if a version 0 source fires.(在等待源Source 和計時器Timer 之前) |
kCFRunLoopAfterWaiting | Inside the event processing loop after the run loop wakes up, but before processing the event that woke it up. This activity occurs only if the run loop did in fact go to sleep during the current loop.(在等待源Source 和計時器Timer 后,同時在被喚醒之前。) |
kCFRunLoopExit | The exit of the run loop, after exiting the event processing loop. This activity occurs once for each call to CFRunLoopRun and CFRunLoopRunInMode.(runloop的出口) |
kCFRunLoopAllActivities | runloop的所有狀態(tài)。 |
PS:想了解更多的runloop信息,可查看之前彬哥寫的博客。
二、iOS如何監(jiān)控線程卡頓?
說一下QiLagMonitor
中的大致實現(xiàn)思路。
首先,創(chuàng)建一個觀察者
runLoopObserver
,用于觀察主線程的runloop
狀態(tài)。
同時,還要創(chuàng)建一個信號量dispatchSemaphore
,用于保證同步操作。其次,將觀察者
runLoopObserver
添加到主線程runloop
中觀察。然后,開啟一個子線程,并且在子線程中開啟一個持續(xù)的
loop
來監(jiān)控主線程runloop
的狀態(tài)。如果發(fā)現(xiàn)主線程
runloop
的狀態(tài)卡在為BeforeSources
或者AfterWaiting
超過88
毫秒時,即表明主線程當前卡頓。
這時候,我們保存主線程當前的調(diào)用堆棧即可達成監(jiān)控目的。
圖解原理:
- 正常情況下:
- 異常情況下:
三、QiLagMonitor中的具體實現(xiàn)
- 第一步,創(chuàng)建一個信號量
dispatchSemaphore
和觀察者runLoopObserver
。
//! 創(chuàng)建一個信號量,保證同步操作
dispatchSemaphore = dispatch_semaphore_create(0); //! Dispatch Semaphore保證同步
//! 創(chuàng)建一個觀察者
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
同時當主線程的runloop
狀態(tài)發(fā)生改變時,會調(diào)用runLoopObserverCallBack
方法,它內(nèi)部會存儲當前的runloop
狀態(tài)。同時,控制信號量。
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
QiLagMonitor *lagMonitor = (__bridge QiLagMonitor*)info;
lagMonitor->runLoopActivity = activity;
dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
dispatch_semaphore_signal(semaphore);
}
- 第二步,將觀察者添加到主線程
runloop
的common
模式下觀察。
//! 將觀察者添加到主線程runloop的common模式下的觀察中
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
- 第三步,創(chuàng)建一個子線程,并開啟一個持續(xù)的
loop
(其實就是個while
死循環(huán))來監(jiān)控主線程的runloop
狀態(tài)。當runloop
的狀態(tài)持續(xù)為BeforeSources
、AfterWaiting
兩個狀態(tài)時,說明主線程卡頓,記錄當前主線程調(diào)用堆棧。
//! 創(chuàng)建子線程監(jiān)控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//! 子線程開啟一個持續(xù)的loop用來進行監(jiān)控
while (YES) {
long semaphoreWait = dispatch_semaphore_wait(self->dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, STUCKMONITORRATE * NSEC_PER_MSEC));
if (semaphoreWait != 0) {
if (!self->runLoopObserver) {
self->timeoutCount = 0;
self->dispatchSemaphore = 0;
self->runLoopActivity = 0;
return;
}
//! 兩個runloop的狀態(tài),BeforeSources和AfterWaiting這兩個狀態(tài)區(qū)間時間能夠檢測到是否卡頓
if (self->runLoopActivity == kCFRunLoopBeforeSources || self->runLoopActivity == kCFRunLoopAfterWaiting) {
//! 出現(xiàn)三次出結(jié)果
if (++self->timeoutCount < 3) {
continue;
}
NSLog(@"monitor trigger");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSString *stackStr = [QiCallStack callStackWithType:QiCallStackTypeMain];
QiCallStackModel *model = [[QiCallStackModel alloc] init];
model.stackStr = stackStr;
model.isStuck = YES;
[[[QiLagDB shareInstance] increaseWithStackModel:model] subscribeNext:^(id x) {}];
});
} // end activity
}// end semaphore wait
self->timeoutCount = 0;
}// end while
});
推薦文章:
元旦福利!QiShare給大家發(fā)2020新年紅包啦~
iOS 性能監(jiān)控(一)—— CPU功耗監(jiān)控
初識Flutter web
用SwiftUI給視圖添加動畫
用SwiftUI寫一個簡單頁面
iOS App啟動優(yōu)化(三)—— 自己做一個工具監(jiān)控App的啟動耗時
iOS App啟動優(yōu)化(二)—— 使用“Time Profiler”工具監(jiān)控App的啟動耗時
iOS App啟動優(yōu)化(一)—— 了解App的啟動流程