卡頓主要表現(xiàn)為主線程卡死讨盒,不響應(yīng)用戶動作或者響應(yīng)很慢解取,這種體驗(yàn)很差,會讓用戶對產(chǎn)品的認(rèn)可度急速下滑返顺,如果不及時優(yōu)化禀苦,最終會導(dǎo)致用戶流失。
那么遂鹊,哪些情況會導(dǎo)致主線程卡頓呢振乏?大體有如下幾個方面:
- 很復(fù)雜的 UI 、圖文混排的繪制量很大秉扑;
- 主線程進(jìn)行網(wǎng)絡(luò)同步請求慧邮;
- 主線程上做大量的 IO 操作;
- 運(yùn)算量過大邻储,CPU 持續(xù)高占用赋咽;
- 死鎖和主子線程搶鎖旧噪。
檢測方案
為了優(yōu)化卡頓吨娜,我們需要準(zhǔn)確的知道哪里發(fā)生了卡頓,然后才能有針對性的進(jìn)行優(yōu)化淘钟,所以在開始優(yōu)化之前我們需要去監(jiān)控卡頓發(fā)生的地方宦赠。那么問題來了,怎么監(jiān)控卡頓米母?
檢測 FPS 變化幅度是一種方案勾扭,但是并不推薦,原因我引用戴銘大佬在如何利用 RunLoop 原理去監(jiān)控卡頓铁瞒?一文中的描述:”FPS 是一秒顯示的幀數(shù)妙色,也就是一秒內(nèi)畫面變化數(shù)量。如果按照動畫片來說慧耍,動畫片的 FPS 就是 24身辨,是達(dá)不到 60 滿幀的。也就是說芍碧,對于動畫片來說煌珊,24 幀時雖然沒有 60 幀時流暢,但也已經(jīng)是連貫的了泌豆,所以并不能說 24 幀時就算是卡住了定庵。“
另一種推薦的方案就是 RunLoop。為什么Runloop可以做到卡頓監(jiān)控蔬浙?我們知道程序中的任務(wù)都是在線程中執(zhí)行猪落,而線程依賴于 RunLoop,并且RunLoop總是在相應(yīng)的狀態(tài)下執(zhí)行任務(wù)畴博,執(zhí)行完成以后會切換到下一個狀態(tài)许布,如果在一個狀態(tài)下執(zhí)行時間過長導(dǎo)致無法進(jìn)入下一個狀態(tài)就可以認(rèn)為發(fā)生了卡頓,所以可以根據(jù)主線程 RunLoop 的狀態(tài)變化檢測任務(wù)執(zhí)行時間是否太長绎晃。至于多長時間算作卡頓可以依據(jù)自己的需要來設(shè)置蜜唾,一般情況下可以設(shè)置1秒鐘作為閥值。
RunLoop 的狀態(tài)如下:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 進(jìn)入Runloop
kCFRunLoopBeforeTimers = (1UL << 1), // 處理Timer事件
kCFRunLoopBeforeSources = (1UL << 2), // 處理Source事件
kCFRunLoopBeforeWaiting = (1UL << 5), // 進(jìn)入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 喚醒
kCFRunLoopExit = (1UL << 7), // 退出Runloop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有狀態(tài)
};
RunLoop 的執(zhí)行流程:
在一次循環(huán)中庶艾,Timer事件袁余、Source事件、喚醒后事件如果處理時間過長都可以認(rèn)為卡頓了咱揍;當(dāng)然還有一種休眠前的事件颖榜,但是監(jiān)控這個事件時需要特別小心,因?yàn)椴荒馨研菝叩臅r間算作是卡頓的煤裙。
具體實(shí)現(xiàn)
大體的思路有了掩完,那怎么來實(shí)現(xiàn)呢?要監(jiān)控 RunLoop 事件硼砰,首先需要一個觀察者:
CFRunLoopObserverContext context = {
0, // 直接傳0就好
(__bridge void*)self, // 對應(yīng)回調(diào)中地方 void *info 參數(shù)
&CFRetain, // 內(nèi)存管理方案
&CFRelease, // 內(nèi)存管理方案
NULL
};
observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runloopObserverCallback, &context);
觀察主線程:
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
在回調(diào)函數(shù)中且蓬,需要記錄下當(dāng)前的模式以便于后面檢測任務(wù)的處理:
static void runloopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
[LagMonitor shared]->currentActivity = activity;
dispatch_semaphore_t sema = [LagMonitor shared]->semaphore;
dispatch_semaphore_signal(sema);
}
然后,不能在主線程中進(jìn)行觀察任務(wù)题翰,因?yàn)槲覀冇^測的是主線程本身的任務(wù)恶阴,把觀察后的處理任務(wù)也加到主線程會使得主線程任務(wù)不純粹,影響檢測結(jié)果的準(zhǔn)確性豹障。所以冯事,我們在子線程中處理檢測任務(wù),相應(yīng)的代碼和釋義如下:
// 在子線程中監(jiān)控卡頓
semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 開啟持續(xù)的loop來監(jiān)控
while ([LagMonitor shared]->isMonitoring) {
if ([LagMonitor shared]->currentActivity == kCFRunLoopBeforeWaiting)
{
// 處理休眠前事件觀測
__block BOOL timeOut = YES;
dispatch_async(dispatch_get_main_queue(), ^{
timeOut = NO; // timeOut任務(wù)
});
[NSThread sleepForTimeInterval:WAIT_TIME];
// WAIT_TIME 時間后,如果 timeOut任務(wù) 任未執(zhí)行, 則認(rèn)為主線程前面的任務(wù)執(zhí)行時間過長導(dǎo)致卡頓
if (timeOut) {
[LXDBacktraceLogger lxd_logMain]; // 輸出堆棧信息
}
}
else
{
// 處理 Timer,Source,喚醒后事件
// 同步等待時間內(nèi),接收到信號result=0, 超時則繼續(xù)往下執(zhí)行并且result!=0
long result = dispatch_semaphore_wait([LagMonitor shared]->semaphore, dispatch_time(DISPATCH_TIME_NOW, OUT_TIME));
if (result != 0) { // 超時
if (![LagMonitor shared]->observer) {
[[LagMonitor shared] endMonitor];
continue;
}
if ([LagMonitor shared]->currentActivity == kCFRunLoopBeforeSources ||
[LagMonitor shared]->currentActivity == kCFRunLoopAfterWaiting ||
[LagMonitor shared]->currentActivity == kCFRunLoopBeforeTimers) {
[LXDBacktraceLogger lxd_logMain]; // 輸出堆棧信息
}
}
}
}
});
項(xiàng)目的全部代碼都在 這里 血公,其中 [LXDBacktraceLogger lxd_logMain]
使用了 LXDAppFluecyMonitor 中的開源代碼輸出堆棧信息昵仅。
檢測效果
我們運(yùn)行看一下效果,首先調(diào)用
[[LagMonitor shared] beginMonitor];
查看日志輸出:
runloop卡頓監(jiān)控[45103:2594859] touchesBegan
runloop卡頓監(jiān)控[45103:2595184] 主線程卡頓 Backtrace of Thread 771:
======================================================================================
libsystem_kernel.dylib 0x7fff5e703756 __semwait_signal + 10
Foundation 0x7fff2085188c +[NSThread sleepForTimeInterval:] + 170
runloop??°è°?á??êé? 0x107bbde06 -[ViewController touchesBegan:withEvent:] + 118
UIKitCore 0x7fff246a8b63 forwardTouchMethod + 321
UIKitCore 0x7fff246a8a11 -[UIResponder touchesBegan:withEvent:] + 49
UIKitCore 0x7fff246b7ad1 -[UIWindow _sendTouchesForEvent:] + 622
UIKitCore 0x7fff246b9be3 -[UIWindow sendEvent:] + 4774
UIKitCore 0x7fff246938f6 -[UIApplication sendEvent:] + 633
UIKitCore 0x7fff2472439c __processEventQueue + 13895
UIKitCore 0x7fff2471ad0f __eventFetcherSourceCallback + 104
CoreFoundation 0x7fff2038c37a __CFRUNLOOP_IS_CALLING_OUT_TO_A_SO
日志顯示累魔,在 -[ViewController touchesBegan:withEvent:]
中有+[NSThread sleepForTimeInterval:]
發(fā)生了卡頓摔笤,回到項(xiàng)目中檢查代碼:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesBegan");
[NSThread sleepForTimeInterval:2];
}
與日志符合,這里確實(shí)發(fā)生了卡頓薛夜,就可以有針對性的進(jìn)行優(yōu)化籍茧。
項(xiàng)目地址:runloop卡頓監(jiān)控
參考資料:
深入理解RunLoop,ibireme