前言:
最近,在看戴銘老師關(guān)于 “性能監(jiān)控” 相關(guān)的技術(shù)分享欧穴,感覺收獲很多改艇『富#基于最近的學(xué)習(xí)故痊,總結(jié)了一些性能監(jiān)控相關(guān)的實(shí)踐顶瞳,并計(jì)劃落地一系列 “性能監(jiān)控” 相關(guān)的文章。
目錄如下:
iOS 性能監(jiān)控(一)—— CPU功耗監(jiān)控
iOS 性能監(jiān)控(二)—— 主線程卡頓監(jiān)控
iOS 性能監(jiān)控(三)—— 方法耗時(shí)監(jiān)控
本篇將介紹iOS性能監(jiān)控工具(QiLagMonitor)中與 “線程卡頓監(jiān)控” 相關(guān)的功能模塊愕秫。
一慨菱、了解線程的狀態(tài)
主線程runloop
默認(rèn)注冊了五個(gè)mode
:kCFRunLoopDefaultMode
、UITrackingRunLoopMode
戴甩、UIInitializationRunLoopMode
符喝、GSEventReceiveRunLoopMode
、kCFRunLoopCommonModes
甜孤。
名稱 | 作用 |
---|---|
kCFRunLoopDefaultMode | App的默認(rèn) Mode协饲,通常主線程是在這個(gè) Mode 下運(yùn)行的畏腕。 |
UITrackingRunLoopMode | 界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動(dòng)茉稠,保證界面滑動(dòng)時(shí)不受其他 Mode 影響描馅。 |
UIInitializationRunLoopMode | 剛啟動(dòng) App 時(shí)第進(jìn)入的第一個(gè) Mode,啟動(dòng)完成后就不再使用而线。 |
GSEventReceiveRunLoopMode | 接受系統(tǒng)事件的內(nèi)部 Mode铭污,通常用不到。 |
kCFRunLoopCommonModes | 這是一個(gè)占位的 Mode膀篮。其實(shí)就是Default模式和UI模式之間切換使用嘹狞。 |
其中,APPLE
公開提供的Mode
有兩個(gè):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 | 運(yùn)行runloop的入口。 |
kCFRunLoopBeforeTimers | Inside the event processing loop before any timers are processed.(在處理任何Timer 計(jì)時(shí)器之前) |
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 和計(jì)時(shí)器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 和計(jì)時(shí)器Timer 后速蕊,同時(shí)在被喚醒之前嫂丙。) |
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
中的大致實(shí)現(xiàn)思路隅肥。
首先,創(chuàng)建一個(gè)觀察者
runLoopObserver
袄简,用于觀察主線程的runloop
狀態(tài)腥放。
同時(shí),還要?jiǎng)?chuàng)建一個(gè)信號量dispatchSemaphore
绿语,用于保證同步操作秃症。其次,將觀察者
runLoopObserver
添加到主線程runloop
中觀察吕粹。然后种柑,開啟一個(gè)子線程,并且在子線程中開啟一個(gè)持續(xù)的
loop
來監(jiān)控主線程runloop
的狀態(tài)匹耕。如果發(fā)現(xiàn)主線程
runloop
的狀態(tài)卡在為BeforeSources
或者AfterWaiting
超過88
毫秒時(shí)聚请,即表明主線程當(dāng)前卡頓。
這時(shí)候稳其,我們保存主線程當(dāng)前的調(diào)用堆棧即可達(dá)成監(jiān)控目的驶赏。
圖解原理:
- 正常情況下:
- 異常情況下:
三炸卑、QiLagMonitor中的具體實(shí)現(xiàn)
- 第一步,創(chuàng)建一個(gè)信號量
dispatchSemaphore
和觀察者runLoopObserver
煤傍。
//! 創(chuàng)建一個(gè)信號量矾兜,保證同步操作
dispatchSemaphore = dispatch_semaphore_create(0); //! Dispatch Semaphore保證同步
//! 創(chuàng)建一個(gè)觀察者
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
同時(shí)當(dāng)主線程的runloop
狀態(tài)發(fā)生改變時(shí),會(huì)調(diào)用runLoopObserverCallBack
方法患久,它內(nèi)部會(huì)存儲(chǔ)當(dāng)前的runloop
狀態(tài)椅寺。同時(shí),控制信號量蒋失。
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)建一個(gè)子線程荆萤,并開啟一個(gè)持續(xù)的
loop
(其實(shí)就是個(gè)while
死循環(huán))來監(jiān)控主線程的runloop
狀態(tài)。當(dāng)runloop
的狀態(tài)持續(xù)為BeforeSources
铣卡、AfterWaiting
兩個(gè)狀態(tài)時(shí)链韭,說明主線程卡頓,記錄當(dāng)前主線程調(diào)用堆棧煮落。
//! 創(chuàng)建子線程監(jiān)控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//! 子線程開啟一個(gè)持續(xù)的loop用來進(jìn)行監(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;
}
//! 兩個(gè)runloop的狀態(tài)敞峭,BeforeSources和AfterWaiting這兩個(gè)狀態(tài)區(qū)間時(shí)間能夠檢測到是否卡頓
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
});
最后,本系列我是站在iOS業(yè)界巨人的肩膀上完成的蝉仇,感謝戴銘老師精彩的技術(shù)分享旋讹。 祝大家學(xué)有所成,工作順利轿衔。
另附上沉迹,戴銘老師課程鏈接:《iOS開發(fā)高手課》,謝謝害驹!