淺談
前段時(shí)間有個(gè)客戶問(wèn)我侧戴,為啥你們項(xiàng)目都搞了好幾年了宁昭,為啥線上還會(huì)經(jīng)常反饋卡頓,呃呃呃酗宋。积仗。
于是根據(jù)自己的理解以及網(wǎng)上大佬們的思路總結(jié)了一篇關(guān)于卡頓優(yōu)化這塊的文章。
卡頓問(wèn)題是一個(gè)老生常談的話題了蜕猫,一個(gè)App的好壞寂曹,卡頓也許會(huì)占一半,它直接決定了用戶的留存問(wèn)題丹锹,各大app排行版上稀颁,那些知名度較高芬失,但是排行較低的楣黍,可能就要思考思考是不是和你app本身有關(guān)系了。
卡頓一直是性能優(yōu)化中相對(duì)重要的一個(gè)點(diǎn)棱烂,因?yàn)槠渖婕傲?strong>UI繪制租漂、垃圾回收(GC)、線程調(diào)度以及Binder颊糜,CPU哩治,GPU方面等JVM以及FrameWork相關(guān)知識(shí)
如果能做好卡頓優(yōu)化,那么也就間接證明你對(duì)Android FrameWork的理解之深衬鱼。
下面兩篇是筆者之前總結(jié)的兩篇關(guān)于啟動(dòng)優(yōu)化和內(nèi)存優(yōu)化的文章:
Android 性能優(yōu)化(一): 啟動(dòng)優(yōu)化理論與實(shí)踐
Android性能優(yōu)化(二):內(nèi)存優(yōu)化你一定要了解的知識(shí)點(diǎn)
下面我們就來(lái)講解下卡頓方面的知識(shí)业筏。
什么是卡頓:
對(duì)用戶來(lái)講就是界面不流暢,滯頓鸟赫。 場(chǎng)景如下:
- 1.視頻加載慢蒜胖,畫(huà)面卡頓消别,卡死,黑屏
- 2.聲音卡頓台谢,音畫(huà)不同步寻狂。
- 3.動(dòng)畫(huà)幀卡頓,交互響應(yīng)慢
- 4.滑動(dòng)不跟手朋沮,列表自動(dòng)更新蛇券,滾動(dòng)不流暢
- 5.網(wǎng)絡(luò)響應(yīng)慢,數(shù)據(jù)和畫(huà)面展示慢樊拓、
- 6.過(guò)渡動(dòng)畫(huà)生硬纠亚。
- 7.界面不可交互,卡死筋夏,等等現(xiàn)象菜枷。
卡頓是如何發(fā)生的
卡頓產(chǎn)生的原因一般都比較復(fù)雜,如CPU內(nèi)存大小叁丧,IO操作啤誊,鎖操作,低效的算法等都會(huì)引起卡頓拥娄。
站在開(kāi)發(fā)的角度看: 通常我們講蚊锹,屏幕刷新率是60fps,需要在16ms內(nèi)完成所有的工作才不會(huì)造成卡頓稚瘾。
為什么是16ms牡昆,不是17,18呢摊欠?
下面我們先來(lái)理清在UI繪制中的幾個(gè)概念:
SurfaceFlinger:
SurfaceFlinger作用是接受多個(gè)來(lái)源的圖形顯示數(shù)據(jù)Surface丢烘,合成后發(fā)送到顯示設(shè)備,比如我們的主界面中:可能會(huì)有statusBar,側(cè)滑菜單些椒,主界面播瞳,這些View都是獨(dú)立Surface渲染和更新,最后提交給SF后免糕,SF根據(jù)Zorder赢乓,透明度,大小石窑,位置等參數(shù)牌芋,合成為一個(gè)數(shù)據(jù)buffer,傳遞HWComposer或者OpenGL處理松逊,最終給顯示器躺屁。
在顯示過(guò)程中使用到了bufferqueue,surfaceflinger作為consumer方经宏,比如windowmanager管理的surface作為生產(chǎn)方產(chǎn)生頁(yè)面犀暑,交由surfaceflinger進(jìn)行合成熄捍。
VSYNC
Android系統(tǒng)每隔16ms發(fā)出VSYNC信號(hào),觸發(fā)對(duì)UI進(jìn)行渲染母怜,VSYNC是一種在PC上很早就有應(yīng)用余耽,可以理解為一種定時(shí)中斷技術(shù)。
tearing 問(wèn)題:
早期的 Android 是沒(méi)有 vsync 機(jī)制的苹熏,CPU 和 GPU 的配合也比較混亂碟贾,這也造成著名的 tearing 問(wèn)題,即 CPU/GPU 直接更新正在顯示的屏幕 buffer 造成畫(huà)面撕裂轨域。 后續(xù) Android 引入了雙緩沖機(jī)制袱耽,但是 buffer 的切換也需要一個(gè)比較合適的時(shí)機(jī),也就是屏幕掃描完上一幀后的時(shí)機(jī)干发,這也就是引入 vsync
的原因朱巨。
早先一般的屏幕刷新率是 60fps,所以每個(gè) vsync
信號(hào)的間隔也是 16ms枉长,不過(guò)隨著技術(shù)的更迭以及廠商對(duì)于流暢性的追求冀续,越來(lái)越多 90fps 和 120fps 的手機(jī)面世,相對(duì)應(yīng)的間隔也就變成了 11ms 和 8ms必峰。
VSYNC信號(hào)種類(lèi):
- 1.屏幕產(chǎn)生的硬件VSYNC:硬件
VSYNC
是一種脈沖信號(hào)洪唐,起到開(kāi)關(guān)和觸發(fā)某種操作的作用。 - 2.由
SurfaceFlinger
將其轉(zhuǎn)成的軟件VSYNC信號(hào)吼蚁,經(jīng)由Binder
傳遞給Choreographer
Choreographer:
編舞者
凭需,用于注冊(cè)VSYNC信號(hào)并接收VSYNC信號(hào)回調(diào),當(dāng)內(nèi)部接收到這個(gè)信號(hào)時(shí)最終會(huì)調(diào)用到doFrame進(jìn)行幀的繪制操作肝匆。
Choreographer在系統(tǒng)中流程
:
如何通過(guò)Choreographer計(jì)算掉幀
情況:原理就是:
通過(guò)給Choreographer設(shè)置FrameCallback粒蜈,在每次繪制前后看時(shí)間差是16.6ms的多少倍,即為前后掉幀率旗国。
使用方式如下:
//Application.java
public void onCreate() {
super.onCreate();
//在Application中使用postFrameCallback
Choreographer.getInstance().postFrameCallback(new FPSFrameCallback(System.nanoTime()));
}
public class FPSFrameCallback implements Choreographer.FrameCallback {
private static final String TAG = "FPS_TEST";
private long mLastFrameTimeNanos = 0;
private long mFrameIntervalNanos;
public FPSFrameCallback(long lastFrameTimeNanos) {
mLastFrameTimeNanos = lastFrameTimeNanos;
mFrameIntervalNanos = (long)(1000000000 / 60.0);
}
@Override
public void doFrame(long frameTimeNanos) {
//初始化時(shí)間
if (mLastFrameTimeNanos == 0) {
mLastFrameTimeNanos = frameTimeNanos;
}
final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if(skippedFrames>30){
//丟幀30以上打印日志
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
}
mLastFrameTimeNanos=frameTimeNanos;
//注冊(cè)下一幀回調(diào)
Choreographer.getInstance().postFrameCallback(this);
}
}
UI繪制全路徑分析:
有了前面幾個(gè)概念枯怖,這里我們讓SurfaceFlinger結(jié)合View的繪制流程用一張圖來(lái)表達(dá)整個(gè)繪制流程:
- 生產(chǎn)者:APP方構(gòu)建Surface的過(guò)程。
- 消費(fèi)者:SurfaceFlinger
UI繪制全路徑分析卡頓原因:
接下來(lái)粗仓,我們逐個(gè)分析嫁怀,看看都會(huì)有哪些原因可能造成卡頓:
1.渲染流程
1.Vsync 調(diào)度:這個(gè)是起始點(diǎn),但是調(diào)度的過(guò)程會(huì)經(jīng)過(guò)線程切換以及一些委派的邏輯借浊,有可能造成卡頓,但是一般可能性比較小萝招,我們也基本無(wú)法介入蚂斤;
2.消息調(diào)度:主要是 doframe Message 的調(diào)度,這就是一個(gè)普通的 Handler 調(diào)度槐沼,如果這個(gè)調(diào)度被其他的 Message 阻塞產(chǎn)生了時(shí)延曙蒸,會(huì)直接導(dǎo)致后續(xù)的所有流程不會(huì)被觸發(fā)
3.input 處理:input 是一次 Vsync 調(diào)度最先執(zhí)行的邏輯捌治,主要處理 input 事件。如果有大量的事件堆積或者在事件分發(fā)邏輯中加入大量耗時(shí)業(yè)務(wù)邏輯纽窟,會(huì)造成當(dāng)前幀的時(shí)長(zhǎng)被拉大肖油,造成卡頓,可以嘗試通過(guò)事件采樣的方案臂港,減少 event 的處理
4.動(dòng)畫(huà)處理:主要是 animator 動(dòng)畫(huà)的更新森枪,同理,動(dòng)畫(huà)數(shù)量過(guò)多审孽,或者動(dòng)畫(huà)的更新中有比較耗時(shí)的邏輯县袱,也會(huì)造成當(dāng)前幀的渲染卡頓。對(duì)動(dòng)畫(huà)的降幀和降復(fù)雜度其實(shí)解決的就是這個(gè)問(wèn)題佑力;
5.view 處理:主要是接下來(lái)的三大流程式散,過(guò)度繪制、頻繁刷新打颤、復(fù)雜的視圖效果都是此處造成卡頓的主要原因暴拄。比如我們平時(shí)所說(shuō)的降低頁(yè)面層級(jí),主要解決的就是這個(gè)問(wèn)題编饺;
6.measure/layout/draw:view 渲染的三大流程揍移,因?yàn)樯婕暗奖闅v和高頻執(zhí)行,所以這里涉及到的耗時(shí)問(wèn)題均會(huì)被放大反肋,比如我們會(huì)降不能在 draw 里面調(diào)用耗時(shí)函數(shù)那伐,不能 new 對(duì)象等等;
7.DisplayList 的更新:這里主要是 canvas 和 displaylist 的映射石蔗,一般不會(huì)存在卡頓問(wèn)題罕邀,反而可能存在映射失敗導(dǎo)致的顯示問(wèn)題;
8.OpenGL 指令轉(zhuǎn)換:這里主要是將 canvas 的命令轉(zhuǎn)換為 OpenGL 的指令养距,一般不存在問(wèn)題
9.buffer 交換:這里主要指 OpenGL 指令集交換給 GPU诉探,這個(gè)一般和指令的復(fù)雜度有關(guān)
10.GPU 處理:顧名思義,這里是 GPU 對(duì)數(shù)據(jù)的處理棍厌,耗時(shí)主要和任務(wù)量和紋理復(fù)雜度有關(guān)肾胯。這也就是我們降低 GPU 負(fù)載有助于降低卡頓的原因;
11.layer 合成:Android P 修改了 Layer 的計(jì)算方法 , 把這部分放到了 SurfaceFlinger 主線程去執(zhí)行, 如果后臺(tái) Layer 過(guò)多, 就會(huì)導(dǎo)致 SurfaceFlinger 在執(zhí)行 rebuildLayerStacks 的時(shí)候耗時(shí) , 導(dǎo)致 SurfaceFlinger 主線程執(zhí)行時(shí)間過(guò)長(zhǎng)耘纱。 可以選擇降低Surface層級(jí)來(lái)優(yōu)化卡頓敬肚。
- 12.光柵化/Display:這里暫時(shí)忽略,底層系統(tǒng)行為束析; Buffer 切換:主要是屏幕的顯示艳馒,這里 buffer 的數(shù)量也會(huì)影響幀的整體延遲,不過(guò)是系統(tǒng)行為,不能干預(yù)弄慰。
2.系統(tǒng)負(fù)載
- 內(nèi)存:內(nèi)存的吃緊會(huì)直接導(dǎo)致 GC 的增加甚至 ANR第美,是造成卡頓的一個(gè)不可忽視的因素;
-
CPU:CPU 對(duì)卡頓的影響主要在于線程調(diào)度慢陆爽、任務(wù)執(zhí)行的慢和資源競(jìng)爭(zhēng)什往,比如
1.降頻會(huì)直接導(dǎo)致應(yīng)用卡頓;
2.后臺(tái)活動(dòng)進(jìn)程太多導(dǎo)致系統(tǒng)繁忙慌闭,cpu \ io \ memory 等資源都會(huì)被占用, 這時(shí)候很容易出現(xiàn)卡頓問(wèn)題 别威,這種情況比較常見(jiàn),可以使用dumpsys cpuinfo查看當(dāng)前設(shè)備的cpu使用情況:
3.主線程調(diào)度不到 , 處于 Runnable 狀態(tài),這種情況比較少見(jiàn)
4.System 鎖:system_server 的 AMS 鎖和 WMS 鎖 , 在系統(tǒng)異常的情況下 , 會(huì)變得非常嚴(yán)重 , 如下圖所示 , 許多系統(tǒng)的關(guān)鍵任務(wù)都被阻塞 , 等待鎖的釋放 , 這時(shí)候如果有 App 發(fā)來(lái)的 Binder 請(qǐng)求帶鎖 , 那么也會(huì)進(jìn)入等待狀態(tài) , 這時(shí)候 App 就會(huì)產(chǎn)生性能問(wèn)題 ; 如果此時(shí)做 Window 動(dòng)畫(huà) , 那么 system_server 的這些鎖也會(huì)導(dǎo)致窗口動(dòng)畫(huà)卡頓
- GPU:GPU 的影響見(jiàn)渲染流程贡必,但是其實(shí)還會(huì)間接影響到功耗和發(fā)熱兔港;
- 功耗/發(fā)熱:功耗和發(fā)熱一般是不分家的,高功耗會(huì)引起高發(fā)熱仔拟,進(jìn)而會(huì)引起系統(tǒng)保護(hù)衫樊,比如降頻、熱緩解等利花,間接的導(dǎo)致卡頓科侈。
如何監(jiān)控卡頓
線下監(jiān)控:
我們知道卡頓問(wèn)題的原因錯(cuò)綜復(fù)雜,但最終都可以反饋到CPU使用率上來(lái)
1.使用dumpsys cpuinfo命令
這個(gè)命令可以獲取當(dāng)時(shí)設(shè)備cpu使用情況炒事,我們可以在線下通過(guò)重度使用應(yīng)用來(lái)檢測(cè)可能存在的卡頓點(diǎn)
A8S:/ $ dumpsys cpuinfo
Load: 1.12 / 1.12 / 1.09
CPU usage from 484321ms to 184247ms ago (2022-11-02 14:48:30.793 to 2022-11-02 1
4:53:30.866):
2% 1053/scanserver: 0.2% user + 1.7% kernel
0.6% 934/system_server: 0.4% user + 0.1% kernel / faults: 563 minor
0.4% 564/signserver: 0% user + 0.4% kernel
0.2% 256/ueventd: 0.1% user + 0% kernel / faults: 320 minor
0.2% 474/surfaceflinger: 0.1% user + 0.1% kernel
0.1% 576/vendor.sprd.hardware.gnss@2.0-service: 0.1% user + 0% kernel / faults
: 54 minor
0.1% 286/logd: 0% user + 0% kernel / faults: 10 minor
0.1% 2821/com.allinpay.appstore: 0.1% user + 0% kernel / faults: 1312 minor
0.1% 447/android.hardware.health@2.0-service: 0% user + 0% kernel / faults: 11
75 minor
0% 1855/com.smartpos.dataacqservice: 0% user + 0% kernel / faults: 755 minor
0% 2875/com.allinpay.appstore:pushcore: 0% user + 0% kernel / faults: 744 mino
r
0% 1191/com.android.systemui: 0% user + 0% kernel / faults: 70 minor
0% 1774/com.android.nfc: 0% user + 0% kernel
0% 172/kworker/1:2: 0% user + 0% kernel
0% 145/irq/24-70900000: 0% user + 0% kernel
0% 575/thermald: 0% user + 0% kernel / faults: 300 minor
...
2.CPU Profiler
這個(gè)工具是AS自帶的CPU性能檢測(cè)工具臀栈,可以在PC上實(shí)時(shí)查看我們CPU使用情況。 AS提供了四種Profiling Model配置:
- 1.
Sample Java Methods
:在應(yīng)用程序基于Java的代碼執(zhí)行過(guò)程中挠乳,頻繁捕獲應(yīng)用程序的調(diào)用堆棧 獲取有關(guān)應(yīng)用程序基于Java的代碼執(zhí)行的時(shí)間和資源使用情況信息权薯。 - 2.
Trace java methods
:在運(yùn)行時(shí)對(duì)應(yīng)用程序進(jìn)行檢測(cè),以在每個(gè)方法調(diào)用的開(kāi)始和結(jié)束時(shí)記錄時(shí)間戳睡扬。收集時(shí)間戳并進(jìn)行比較以生成方法跟蹤數(shù)據(jù)盟蚣,包括時(shí)序信息和CPU使用率。
請(qǐng)注意與檢測(cè)每種方法相關(guān)的開(kāi)銷(xiāo)會(huì)影響運(yùn)行時(shí)性能卖怜,并可能影響性能分析數(shù)據(jù)屎开。對(duì)于生命周期相對(duì)較短的方法,這一點(diǎn)甚至更為明顯马靠。此外奄抽,如果您的應(yīng)用在短時(shí)間內(nèi)執(zhí)行大量方法,則探查器可能會(huì)很快超過(guò)其文件大小限制甩鳄,并且可能無(wú)法記錄任何進(jìn)一步的跟蹤數(shù)據(jù)逞度。
- 3.
Sample C/C++ Functions
:捕獲應(yīng)用程序本機(jī)線程的示例跟蹤。要使用此配置娩贷,您必須將應(yīng)用程序部署到運(yùn)行Android 8.0(API級(jí)別26)或更高版本的設(shè)備第晰。 - 4.
Trace System Calls
:捕獲細(xì)粒度的詳細(xì)信息,使您可以檢查應(yīng)用程序與系統(tǒng)資源的交互方式 您可以檢查線程狀態(tài)的確切時(shí)間和持續(xù)時(shí)間彬祖,可視化CPU瓶頸在所有內(nèi)核中的位置茁瘦,并添加自定義跟蹤事件進(jìn)行分析。在對(duì)性能問(wèn)題進(jìn)行故障排除時(shí)卖鲤,此類(lèi)信息可能至關(guān)重要较锡。要使用此配置只搁,您必須將應(yīng)用程序部署到運(yùn)行Android 7.0(API級(jí)別24)或更高版本的設(shè)備。
使用方式:
Debug.startMethodTracing("");
// 需要檢測(cè)的代碼片段
...
Debug.stopMethodTracing();
優(yōu)點(diǎn)
:**有比較全面的調(diào)用棧以及圖像化方法時(shí)間顯示腔稀,包含所有線程的情況
缺點(diǎn)
:本身也會(huì)帶來(lái)一點(diǎn)的性能開(kāi)銷(xiāo),可能會(huì)帶偏優(yōu)化方向**
火焰圖:可以顯示當(dāng)前應(yīng)用的方法堆棧:
3.Systrace
Systrace在前面一篇分析啟動(dòng)優(yōu)化的文章講解過(guò)
這里我們簡(jiǎn)單來(lái)復(fù)習(xí)下:
Systrace用來(lái)記錄當(dāng)前應(yīng)用的系統(tǒng)以及應(yīng)用(使用Trace類(lèi)打點(diǎn))的各階段耗時(shí)信息包括繪制信息以及CPU信息等羽历。
使用方式:
Trace.beginSection("MyApp.onCreate_1");
alt(200);
Trace.endSection();
在命令行中:
python systrace.py -t 5 sched gfx view wm am app webview -a "com.chinaebipay.thirdcall" -o D:\trac1.html
記錄的方法以及CPU中的耗時(shí)情況:
優(yōu)點(diǎn)
:
- 1.輕量級(jí)焊虏,開(kāi)銷(xiāo)小,CPU使用率可以直觀反映
- 2.右側(cè)的Alerts能夠根據(jù)我們應(yīng)用的問(wèn)題給出具體的建議秕磷,比如說(shuō)诵闭,它會(huì)告訴我們App界面的繪制比較慢或者GC比較頻繁。
4.StrictModel
StrictModel是Android提供的一種運(yùn)行時(shí)檢測(cè)機(jī)制澎嚣,用來(lái)幫助開(kāi)發(fā)者自動(dòng)檢測(cè)代碼中不規(guī)范的地方疏尿。 主要和兩部分相關(guān): 1.線程相關(guān) 2.虛擬機(jī)相關(guān)
基礎(chǔ)代碼:
private void initStrictMode() {
// 1、設(shè)置Debug標(biāo)志位易桃,僅僅在線下環(huán)境才使用StrictMode
if (DEV_MODE) {
// 2褥琐、設(shè)置線程策略
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectCustomSlowCalls() //API等級(jí)11,使用StrictMode.noteSlowCode
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyLog() //在Logcat 中打印違規(guī)異常信息
// .penaltyDialog() //也可以直接跳出警報(bào)dialog
// .penaltyDeath() //或者直接崩潰
.build());
// 3晤郑、設(shè)置虛擬機(jī)策略
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
// 給NewsItem對(duì)象的實(shí)例數(shù)量限制為1
.setClassInstanceLimit(NewsItem.class, 1)
.detectLeakedClosableObjects() //API等級(jí)11
.penaltyLog()
.build());
}
}
線上監(jiān)控:
線上需要自動(dòng)化的卡頓檢測(cè)方案來(lái)定位卡頓敌呈,它能記錄卡頓發(fā)生時(shí)的場(chǎng)景。
自動(dòng)化監(jiān)控原理:
采用攔截消息調(diào)度流程造寝,在消息執(zhí)行前埋點(diǎn)計(jì)時(shí)磕洪,當(dāng)耗時(shí)超過(guò)閾值時(shí),則認(rèn)為是一次卡頓匹舞,會(huì)進(jìn)行堆棧抓取和上報(bào)工作
首先褐鸥,我們看下Looper用于執(zhí)行消息循環(huán)的loop()方法,關(guān)鍵代碼如下所示:
/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/
public static void loop() {
...
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
// 1
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
...
try {
// 2
msg.target.dispatchMessage(msg);
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
...
if (logging != null) {
// 3
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
在Looper的loop()方法中赐稽,在其執(zhí)行每一個(gè)消息(注釋2處)的前后都由logging進(jìn)行了一次打印輸出叫榕。可以看到姊舵,在執(zhí)行消息前是輸出的">>>>> Dispatching to "晰绎,在執(zhí)行消息后是輸出的"<<<<< Finished to ",它們打印的日志是不一樣的,我們就可以由此來(lái)判斷消息執(zhí)行的前后時(shí)間點(diǎn)括丁。
具體的實(shí)現(xiàn)可以歸納為如下步驟:
- 1荞下、首先,我們需要使用Looper.getMainLooper().setMessageLogging()去設(shè)置我們自己的Printer實(shí)現(xiàn)類(lèi)去打印輸出logging。這樣尖昏,在每個(gè)message執(zhí)行的之前和之后都會(huì)調(diào)用我們?cè)O(shè)置的這個(gè)Printer實(shí)現(xiàn)類(lèi)仰税。
- 2、如果我們匹配到">>>>> Dispatching to "之后抽诉,我們就可以執(zhí)行一行代碼:也就是在指定的時(shí)間閾值之后陨簇,我們?cè)谧泳€程去執(zhí)行一個(gè)任務(wù),這個(gè)任務(wù)就是去獲取當(dāng)前主線程的堆棧信息以及當(dāng)前的一些場(chǎng)景信息迹淌,比如:內(nèi)存大小河绽、電腦、網(wǎng)絡(luò)狀態(tài)等唉窃。
- 3耙饰、如果在指定的閾值之內(nèi)匹配到了"<<<<< Finished to ",那么說(shuō)明message就被執(zhí)行完成了纹份,則表明此時(shí)沒(méi)有產(chǎn)生我們認(rèn)為的卡頓效果苟跪,那我們就可以將這個(gè)子線程任務(wù)取消掉。
這里我們使用blockcanary
來(lái)做測(cè)試:
BlockCanary
APM是一個(gè)非侵入式的性能監(jiān)控組件矮嫉,可以通過(guò)通知的形式彈出卡頓信息削咆。它的原理就是我們剛剛講述到的卡頓監(jiān)控的實(shí)現(xiàn)原理。 使用方式:
- 1.導(dǎo)入依賴(lài)
implementation 'com.github.markzhai:blockcanary-android:1.5.0'
- Application的onCreate方法中開(kāi)啟卡頓監(jiān)控
// 注意在主進(jìn)程初始化調(diào)用
BlockCanary.install(this, new AppBlockCanaryContext()).start();
- 3.繼承
BlockCanaryContext
類(lèi)去實(shí)現(xiàn)自己的監(jiān)控配置上下文類(lèi)
public class AppBlockCanaryContext extends BlockCanaryContext {
...
...
/**
* 指定判定為卡頓的閾值threshold (in millis),
* 你可以根據(jù)不同設(shè)備的性能去指定不同的閾值
*
* @return threshold in mills
*/
public int provideBlockThreshold() {
return 1000;
}
....
}
- 4.在Activity的onCreate方法中執(zhí)行一個(gè)耗時(shí)操作
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 5.結(jié)果:
可以看到一個(gè)和LeakCanary一樣效果的阻塞可視化堆棧圖
那有了BlockCanary的方法耗時(shí)監(jiān)控方式是不是就可以解百愁了呢蠢笋,呵呵拨齐。有那么容易就好了
根據(jù)原理:我們拿到的是msg執(zhí)行前后的時(shí)間和堆棧信息,如果msg中有幾百上千個(gè)方法昨寞,就無(wú)法確認(rèn)到底是哪個(gè)方法導(dǎo)致的耗時(shí)瞻惋,也有可能是多個(gè)方法堆積導(dǎo)致。
這就導(dǎo)致我們無(wú)法準(zhǔn)確定位哪個(gè)方法是最耗時(shí)的援岩。如圖中:堆棧信息是T2的歼狼,而發(fā)生耗時(shí)的方法可能是T1到T2中任何一個(gè)方法甚至是堆積導(dǎo)致。
那如何優(yōu)化這塊享怀?
這里我們采用字節(jié)跳動(dòng)給我們提供的一個(gè)方案:基于 Sliver trace 的卡頓監(jiān)控體系
Sliver trace
整體流程圖
:
主要包含兩個(gè)方面:
- 檢測(cè)方案: 在監(jiān)控卡頓時(shí)羽峰,首先需要打開(kāi) Sliver 的 trace 記錄能力,Sliver 采樣記錄 trace 執(zhí)行信息添瓷,對(duì)抓取到的堆棧進(jìn)行 diff 聚合和緩存梅屉。
同時(shí)基于我們的需要設(shè)置相應(yīng)的卡頓閾值,以 Message 的執(zhí)行耗時(shí)為衡量鳞贷。對(duì)主線程消息調(diào)度流程進(jìn)行攔截坯汤,在消息開(kāi)始分發(fā)執(zhí)行時(shí)埋點(diǎn),在消息執(zhí)行結(jié)束時(shí)計(jì)算消息執(zhí)行耗時(shí)搀愧,當(dāng)消息執(zhí)行耗時(shí)超過(guò)閾值惰聂,則認(rèn)為產(chǎn)生了一次卡頓疆偿。
-
堆棧聚合策略: 當(dāng)卡頓發(fā)生時(shí),我們需要為此次卡頓準(zhǔn)備數(shù)據(jù)搓幌,這部分工作是在端上子線程中完成的杆故,主要是 dump trace 到文件以及過(guò)濾聚合要上報(bào)的堆棧。分為以下幾步:
- 1.拿到緩存的主線程 trace 信息并 dump 到文件中鼻种。
- 2.然后從文件中讀取 trace 信息反番,按照數(shù)據(jù)格式沙热,從最近的方法棧向上追溯叉钥,找到當(dāng)前 Message 包含的全部 trace 信息,并將當(dāng)前 Message 的完整 trace 寫(xiě)入到待上傳的 trace 文件中篙贸,刪除其余 trace 信息投队。
- 3.遍歷當(dāng)前 Message trace,按照(Method 執(zhí)行耗時(shí) > Method 耗時(shí)閾值 & Method 耗時(shí)為該層堆棧中最耗時(shí))為條件過(guò)濾出每一層函數(shù)調(diào)用堆棧的最長(zhǎng)耗時(shí)函數(shù)爵川,構(gòu)成最后要上報(bào)的堆棧鏈路敷鸦,這樣特征堆棧中的每一步都是最耗時(shí)的,且最底層 Method 為最后的耗時(shí)大于閾值的 Method寝贡。
之后扒披,將 trace 文件和堆棧一同上報(bào),這樣的特征堆棧提取策略保證了堆棧聚合的可靠性和準(zhǔn)確性圃泡,保證了上報(bào)到平臺(tái)后堆棧的正確合理聚合碟案,同時(shí)提供了進(jìn)一步分析問(wèn)題的 trace 文件。
可以看到字節(jié)給的是一整套監(jiān)控方案颇蜡,和前面BlockCanary不同之處就在于价说,其是定時(shí)存儲(chǔ)堆棧,緩存风秤,然后使用diff去重的方式鳖目,并上傳到服務(wù)器,可以最大限度的監(jiān)控到可能發(fā)生比較耗時(shí)的方法缤弦。
開(kāi)發(fā)中哪些習(xí)慣會(huì)影響卡頓的發(fā)生
1.布局太亂领迈,層級(jí)太深。
- 1.1:通過(guò)減少冗余或者嵌套布局來(lái)降低視圖層次結(jié)構(gòu)碍沐。比如使用約束布局代替線性布局和相對(duì)布局狸捅。
- 1.2:用 ViewStub 替代在啟動(dòng)過(guò)程中不需要顯示的 UI 控件。
- 1.3:使用自定義 View 替代復(fù)雜的 View 疊加抢韭。
2.主線程耗時(shí)操作
- 2.1:主線程中不要直接操作數(shù)據(jù)庫(kù)薪贫,數(shù)據(jù)庫(kù)的操作應(yīng)該放在數(shù)據(jù)庫(kù)線程中完成。
- 2.2:sharepreference盡量使用apply刻恭,少使用commit瞧省,可以使用MMKV框架來(lái)代替sharepreference扯夭。
- 2.3:網(wǎng)絡(luò)請(qǐng)求回來(lái)的數(shù)據(jù)解析盡量放在子線程中,不要在主線程中進(jìn)行復(fù)制的數(shù)據(jù)解析操作鞍匾。
- 2.4:不要在activity的onResume和onCreate中進(jìn)行耗時(shí)操作交洗,比如大量的計(jì)算等。
- 2.5:不要在 draw 里面調(diào)用耗時(shí)函數(shù)橡淑,不能 new 對(duì)象
3.過(guò)度繪制
過(guò)度繪制是同一個(gè)像素點(diǎn)上被多次繪制构拳,減少過(guò)度繪制一般減少布局背景疊加等方式,如下圖所示右邊是過(guò)度繪制的圖片梁棠。
4.列表
RecyclerView使用優(yōu)化置森,使用DiffUtil和notifyItemDataSetChanged進(jìn)行局部更新等。
5.對(duì)象分配和回收優(yōu)化
自從Android引入 ART 并且在Android 5.0上成為默認(rèn)的運(yùn)行時(shí)之后符糊,對(duì)象分配和垃圾回收(GC)造成的卡頓已經(jīng)顯著降低了凫海,但是由于對(duì)象分配和GC有額外的開(kāi)銷(xiāo),它依然又可能使線程負(fù)載過(guò)重男娄。 在一個(gè)調(diào)用不頻繁的地方(比如按鈕點(diǎn)擊)分配對(duì)象是沒(méi)有問(wèn)題的行贪,但如果在在一個(gè)被頻繁調(diào)用的緊密的循環(huán)里,就需要避免對(duì)象分配來(lái)降低GC的壓力模闲。
減少小對(duì)象的頻繁分配和回收操作建瘫。
好了,關(guān)于卡頓優(yōu)化的問(wèn)題就講到這里尸折,下篇文章會(huì)對(duì)卡頓中的ANR情況的處理啰脚,這里做個(gè)鋪墊。
參考
“終于懂了” 系列:Android屏幕刷新機(jī)制—VSync翁授、Choreographer 全面理解拣播!
西瓜卡頓 & ANR 優(yōu)化治理及監(jiān)控體系建設(shè)
作者:小余的自習(xí)室
鏈接:https://juejin.cn/post/7161757546875715615