實(shí)現(xiàn)思路
在開始之前苛白,我們先思考一下,界面卡頓是由哪些原因?qū)е碌模?/p>
1.死鎖:主線程拿到鎖 A焚虱,需要獲得鎖 B购裙,而同時(shí)某個(gè)子線程拿了鎖 B,需要鎖 A鹃栽,這樣相互等待就死鎖了躏率。
2.搶鎖:主線程需要訪問 DB,而此時(shí)某個(gè)子線程往 DB 插入大量數(shù)據(jù)民鼓。通常搶鎖的體驗(yàn)是偶爾卡一陣子薇芝,過會(huì)就恢復(fù)了。
3.主線程大量 IO:主線程為了方便直接寫入大量數(shù)據(jù)丰嘉,會(huì)導(dǎo)致界面卡頓夯到。
4.主線程大量計(jì)算:算法不合理,導(dǎo)致主線程某個(gè)函數(shù)占用大量 CPU饮亏。
5.大量的 UI 繪制:復(fù)雜的 UI耍贾、圖文混排等,帶來大量的 UI 繪制路幸。
針對(duì)這些原因荐开,我們可以怎么定位問題呢?
1.死鎖一般會(huì)伴隨 crash简肴,可以通過 crash report 來分析誓焦。
2.搶鎖不好辦,將鎖等待時(shí)間打出來用處不大着帽,我們還需要知道是誰占了鎖杂伟。
3.大量 IO 可以在函數(shù)開始結(jié)束打點(diǎn),將占用時(shí)間打到日志中仍翰。
4.大量計(jì)算同理可以將耗時(shí)打到日志中赫粥。
5.大量 UI 繪制一般是必現(xiàn),還好辦予借;如果是偶現(xiàn)的話越平,想加日志點(diǎn)都沒地方,因?yàn)槭锹谙到y(tǒng)函數(shù)里面灵迫。
如果可以將當(dāng)時(shí)的線程堆棧捕捉下來秦叛,那么上述難題都迎刃而解。主線程在什么函數(shù)哪一行卡住瀑粥,在等什么鎖挣跋,而這個(gè)鎖又是被哪個(gè)子線程的哪個(gè)函數(shù)占用,有了堆棧狞换,我們都可以知道避咆。自然也能知道是慢在UI繪制,還是慢在我們的代碼修噪。
所以查库,思路就是起一個(gè)子線程,監(jiān)控主線程的活動(dòng)情況黄琼,如果發(fā)現(xiàn)有卡頓樊销,就將堆棧 dump 下來。
技術(shù)實(shí)現(xiàn)
1需要解決的問題
原理一旦講出來脏款,好像也不復(fù)雜围苫。魔鬼都是隱藏在細(xì)節(jié)中,效果好不好弛矛,完全由實(shí)現(xiàn)細(xì)節(jié)決定够吩。具體到卡頓檢測,有幾個(gè)問題需要仔細(xì)處理:
怎么知道主線程發(fā)生了卡頓丈氓?
子線程以什么樣的策略和頻率來檢測主線程周循?這個(gè)是要發(fā)布到現(xiàn)網(wǎng)的,如果處理不好万俗,帶來明顯的性能損耗(尤其是電量)湾笛,就不能接受了。
堆棧上報(bào)了上來怎么分類闰歪?直接用 crash report 的分類不適合嚎研。
卡頓 dump 下來的堆棧會(huì)有多頻繁?數(shù)據(jù)量會(huì)有多大?
全量上報(bào)還是抽樣上報(bào)临扮?怎么在問題跟進(jìn)與節(jié)省流量直接平衡论矾?
2判斷標(biāo)準(zhǔn)
怎么判斷主線程是不是發(fā)生了卡頓?一般來說杆勇,用戶感受得到的卡頓大概有三個(gè)特征:
FPS 降低
CPU 占用率很高
主線程 Runloop 執(zhí)行了很久
看起來 FPS 能夠兼容后面兩個(gè)特征贪壳,但是在實(shí)際操作過程中發(fā)現(xiàn) FPS 不好衡量,抖動(dòng)比較大蚜退。而對(duì)于搶鎖或大量 IO 的情況闰靴,光有 CPU 是不行的。所以我們實(shí)際上用到的是下面兩個(gè)準(zhǔn)則:
CPU 占用超過了100%
主線程 Runloop 執(zhí)行了超過2秒
3檢測策略
為了降低檢測帶來的性能損耗钻注,我們仔細(xì)設(shè)計(jì)了檢測線程的策略:
內(nèi)存 dump:每1秒檢查一次蚂且,如果檢查到主線程卡頓,就將所有線程的函數(shù)調(diào)用堆棧 dump 到內(nèi)存中幅恋。
文件 dump:如果內(nèi)存 dump 的堆棧跟上次捕捉到的不一樣杏死,則 dump 到文件中;否則按照斐波那契數(shù)列將檢查時(shí)間遞增(1佳遣,1识埋,2,3零渐,5窒舟,8…)直到?jīng)]有遇到卡頓或卡頓堆棧不一樣。這樣能夠避免同一個(gè)卡頓寫入多個(gè)文件的情況诵盼,也能避免檢測線程圍著同一個(gè)卡頓空轉(zhuǎn)的情況惠豺。
4分類方法
直接用 crash report 的分類方法是不行的,這個(gè)很好理解:最終卡在 lock 函數(shù)的卡頓风宁,外面可能是很多不同的業(yè)務(wù)洁墙,例如可能是讀取消息,可能是讀取聯(lián)系人戒财,等等热监。卡頓監(jiān)控需要仔細(xì)定義自己的分類規(guī)則饮寞⌒⒖福可以是從調(diào)用堆棧的最外層開始?xì)w類,或者是取中間一部分歸類幽崩,或者是取最里面一部分歸類苦始。
各有優(yōu)缺點(diǎn):
最外層歸類:能夠?qū)⑼蝗肟诘目D歸類起來。缺點(diǎn)是層數(shù)不好定慌申,可能外面十來層都是系統(tǒng)調(diào)用陌选,也有可能第一層就是微信的函數(shù)了。
中間層歸類:能夠根據(jù)事先劃分好的“特征值”來歸類。缺點(diǎn)是“特征值”不好定咨油,如果要做到自動(dòng)學(xué)習(xí)生成的話您炉,對(duì)后臺(tái)分析系統(tǒng)要求太高了。
最內(nèi)層歸類:能夠?qū)⑼辉虻目D歸類起來臼勉。缺點(diǎn)是同一分類可能包含不同的業(yè)務(wù)邻吭。
綜合考慮并一一嘗試之后,我們采用了最內(nèi)層歸類的優(yōu)化版宴霸,亦即進(jìn)行二級(jí)歸類。第一級(jí)按照最內(nèi)倒數(shù)2層歸類膏蚓,這樣能夠?qū)⑼辉虻目D集中起來瓢谢;第二級(jí)分類是從第一級(jí)點(diǎn)擊進(jìn)來,然后從最內(nèi)層倒數(shù)4層進(jìn)行歸類驮瞧,這樣能夠?qū)⑼辉虻牟煌瑯I(yè)務(wù)分散歸類起來氓扛。