一、前言
如果現(xiàn)在用戶反饋某個列表很卡,你會怎么排查問題?
這樣一個簡短的問題,其實考察了我們多方面的知識。要答出其中的一兩個小點其實并不難,難的是如何能夠由外之內(nèi),由淺入深娓娓道來,它考察的是一個程序員發(fā)現(xiàn)問題、解決問題、歸納總結(jié)的能力。
要回答這個問題,可以從以下四個方面層層深入,整個大綱如下:
-
(1) 渲染原理
- 為什么會感知到卡頓
- 理解
VSYNC
-
(2) 卡頓的外部因素
- 手機性能
- 系統(tǒng)本身
- 內(nèi)存抖動
- 在主線程執(zhí)行耗時操作
-
(3) 卡頓的內(nèi)部因素
- 布局層級
-
measure、layout、draw耗時 - 過度繪制
-
(4) 監(jiān)控卡頓
- 使用
Handler#setMessageLogging
- 使用
這篇文章中穿插著介紹了性能優(yōu)化工具的使用場景,所有的鏈接地址為:
- 性能優(yōu)化工具知識梳理(1) - TraceView
- 性能優(yōu)化工具知識梳理(2) - Systrace
- 性能優(yōu)化工具知識梳理(3) - 調(diào)試GPU過度繪制 & GPU呈現(xiàn)模式分析
- 性能優(yōu)化工具知識梳理(4) - Hierarchy Viewer
- 性能優(yōu)化工具知識梳理(5) - MAT
- 性能優(yōu)化工具知識梳理(6) - Memory Monitor & Heap Viewer & Allocation Tracker
- 性能優(yōu)化工具知識梳理(7) - LeakCanary
- 性能優(yōu)化工具知識梳理(8) - Lint
二、渲染原理
首先我們需要明白 為什么用戶會感知到卡頓,要回答這個問題,就需要對渲染的原理有一個基本的了解。
2.1 為什么會感知到卡頓
用戶感知到的卡頓主要的根源是因為渲染性能。Android系統(tǒng)每隔16ms發(fā)出VSYNC信號,觸發(fā)對UI進行渲染,如果每次渲染都成功,這樣就能夠達到所需要的60fps,為了能夠?qū)崿F(xiàn)60fps,這意味著程序的大多數(shù)操作都必須在16ms內(nèi)完成。

如果你的某個操作是24ms,系統(tǒng)在得到VSYNC信號的時候就無法進行正常的渲染,這樣就發(fā)生了丟幀現(xiàn)象,那么用戶在32ms內(nèi)看到的是同一幀畫面。

2.2 理解 VSYNC
在理解VSYNC之前,首先需要區(qū)分 幀率 和 刷新率:
- 幀率:代表了
GPU在1s內(nèi) 繪制操作 的幀數(shù),例如30fps、60fps,屬于 軟件參數(shù)。 - 刷新率:代表了屏幕在
1s內(nèi)刷新屏幕的次數(shù),這取決于 硬件的固定參數(shù),例如60Hz。
GPU獲取圖形數(shù)據(jù)進行渲染,然后硬件負責把渲染后的內(nèi)容呈現(xiàn)到屏幕上,兩者不停地協(xié)作。

當幀率和刷新率不一致的時候,就會發(fā)生畫面上下兩部分內(nèi)容斷裂,來自不同的兩幀數(shù)據(jù)發(fā)生重疊。因此引入了VSYNC,在超過60fps的情況下,GPU所產(chǎn)生的幀數(shù)據(jù)會因為等待VSYNC的刷新信號而被Hold住,這樣能夠保持每次刷新都有實際的新的數(shù)據(jù)可以顯示。

但是我們遇到更多的情況是 幀率小于刷新率,也就是我們通常所說的卡頓,如下圖所示。

三、卡頓的外部因素
卡頓的 外因 可以歸結(jié)為以下幾個方面:
- 手機性能問題,
CPU性能不足,內(nèi)存小。 - 系統(tǒng)本身問題,所有應(yīng)用都很卡。
- 頻繁觸發(fā)
GC,導(dǎo)致內(nèi)存抖動。 - 在主線程中進行了耗時的操作。
3.1 手機性能問題
通過排查反饋用戶的機型,如果大部分的反饋都是來自于低端機的用戶,那么可以與產(chǎn)品溝通,通過獲取硬件的相關(guān)參數(shù),例如CPU核數(shù)、內(nèi)存大小,對于這些低端機型進行特殊的處理,對需求進行簡化,避免去實現(xiàn)復(fù)雜的動畫效果。
3.2 系統(tǒng)本身問題
如果可以聯(lián)系用戶,并且用戶反饋不僅是我們的應(yīng)用,而是整個系統(tǒng)都很卡,那么對于我們來說,其實做不了什么。
如果無法聯(lián)系用戶,那么可以通過trace文件進行分析。
3.3 內(nèi)存抖動
內(nèi)存抖動指的是有大量的對象頻繁地進出內(nèi)存的新生代區(qū)域,它往往會伴隨著頻繁的GC,而GC會占用UI線程和CPU資源,從而導(dǎo)致應(yīng)用發(fā)生卡頓,因此我們需要盡量這種現(xiàn)象的發(fā)生。
3.3.1 排查內(nèi)存抖動
在排查內(nèi)存抖動問題的時候,我們可以通過以下幾個工具來輔助排查問題:
-
Memory Monitor:在列表滑動的時候,實時觀察內(nèi)存的分配情況,定位發(fā)生GC的時間點,確定其是否合理,但是其缺點是 無法列出具體的分配對象。 -
Heap Viewer:在垃圾回收的時候,呈現(xiàn)出某一時刻的內(nèi)存快照,幫助我們分析是哪個對象引起了內(nèi)存泄漏。 -
Allocation Tracker:分析出一段時間內(nèi)對象的分配情況,并列出是由什么邏輯導(dǎo)致了這個對象的分配,與Heap Viewer配合使用,來分析大對象產(chǎn)生的原因。
以上這三種工具的詳細使用可以看之前總結(jié)的這篇文章:性能優(yōu)化工具知識梳理(6) - Memory Monitor & Heap Viewer & Allocation Tracker。
3.3.2 容易發(fā)生內(nèi)存抖動的場景
在平時的開發(fā)中,我們可以使用以下幾點來避免內(nèi)存抖動的發(fā)生:
- 在創(chuàng)建對象的操作,移出到循環(huán)體外
- 不要在
onMeasure、onLayout、onDraw方法中頻繁地創(chuàng)建對象,例如Paint、Path這樣的類。 - 在使用
Bitmap的時候,考慮通過LruCache+inBitmap的方式進行復(fù)用。 - 合理地使用對象池來緩存對象。
3.4 在主線程中,執(zhí)行了耗時的操作
3.4.1 排查耗時操作
在排查主線程的耗時操作時,最常用的就是TraceView,通過這個工具可以看到每個方法的具體耗時時間,關(guān)于TraceView的詳細使用可以參考 性能優(yōu)化工具知識梳理(1) - TraceView 這篇文章。
3.4.2 解決主線程耗時問題
在解決主線程耗時問題時,需要根據(jù)具體的業(yè)務(wù)的場景來排查,一般來說,當我們遇到列表卡頓的問題,可以優(yōu)先從以下幾個重要的回調(diào)中排查,看下是否在其中執(zhí)行了耗時的操作,例如IO、JSON等。
-
RecyclerView的onBindViewHolder -
ListView的getView。 -
RecyclerView/ListView的onScrollChanged。
四、卡頓的內(nèi)部因素
- 布局層級
-
measure、layout、draw的耗時 - 過度繪制
4.1 布局層級
當我們設(shè)計列表的每個Item項時,應(yīng)當盡量減少每個Item的布局層級,因為布局層級越深,每個Item繪制就越耗時。
4.1.1 排查布局層級問題
在檢查布局層級問題時,通常是使用Hierarchy Viewer工具,通過該工具可以做到以下兩點:
- 檢查每個
Item項的布局層級 - 通過每個節(jié)點的三個圓點顏色查看其在測量、布局、繪制三個階段的性能表現(xiàn),綠色表示
OK,黃色表示其處于渲染速度比較慢的50%,紅色表示渲染速度非常慢。
更加詳細的介紹可以參考 性能優(yōu)化工具知識梳理(4) - Hierarchy Viewer。
4.1.2 減少布局層級
減少布局層級更多的是需要依賴開發(fā)者的習慣,因為有些時候,越少的層級往往需要更復(fù)雜的設(shè)計邏輯,這意味著需要花更多的時間來思考,在這里強烈推薦ConstraintLayout控件,對于任何復(fù)雜的場景,只需要一層就可以了,使用可以參考 ConstraintLayout 完全解析 快來優(yōu)化你的布局吧。
對于減少布局層級,有以下幾點技巧:
- 首先應(yīng)當考慮布局層級最小的方案。
- 布局層級相同時,就應(yīng)當選取合適的父容器,一般來說,有以下幾點經(jīng)驗:
- 選取的優(yōu)先級為:
FrameLayout、不帶layout_weight參數(shù)的LinearLayout、RelativeLayout,這里選取的標準為帶有layout_weight的LinearLayout或者RelativeLayout會測量兩次。 - 當使用
LinearLayout時,應(yīng)當盡量避免使用layout_weight參數(shù)。 - 避免使用
RelativeLayout嵌套RelativeLayout。 - 如果允許,那么可以使用
Google的ConstraintLayout布局。
更多的技巧可以參考 性能優(yōu)化技巧知識梳理(1) - 布局優(yōu)化。
4.2 measure、layout、draw 的耗時時間
對于這三個階段的耗時,可以通過兩個工具來排查問題:
-
Hierarchy Viewer的節(jié)點參數(shù) - 設(shè)置當中的
GPU呈現(xiàn)模式分析,參考 性能優(yōu)化工具知識梳理(3) - 調(diào)試GPU過度繪制 & GPU呈現(xiàn)模式分析。
當我們發(fā)現(xiàn)在某個階段耗時過長時,就需要去排查是否在以上三個回調(diào)當中做了不當?shù)牟僮鳌?/p>
4.3 過度繪制
過度繪制其實是 布局層級過深的結(jié)果,通過設(shè)置中的 調(diào)試 GPU 過度繪制,可以直觀地看到繪制的重疊情況,檢測的結(jié)果分為以下四種,嚴重程度依次遞增:
- 藍色
- 綠色
- 淺紅
- 深紅
對于過度繪制的部分,需要想辦法去優(yōu)化,詳細的使用方式為:性能優(yōu)化工具知識梳理(3) - 調(diào)試GPU過度繪制 & GPU呈現(xiàn)模式分析。
五、監(jiān)控卡頓
在前面兩節(jié)中,我們從外因和內(nèi)因兩個部分總結(jié)了卡頓問題的排查方法和注意事項,除此之外,還可以通過一些手段實時地監(jiān)控卡頓問題,這里推薦使用Handler的setMessageLogging方法,檢測每個消息的耗時時間,當其耗時大于閾值的時候,輸出堆棧信息。
簡單的實現(xiàn)方式為 Framework 源碼解析知識梳理(4) - 從源碼角度談?wù)?Handler 的應(yīng)用。
BlockCanary就是基于這個原理來實現(xiàn)的,具體的使用方式可以參考 AndroidPerformanceMonitor。
六、參考文獻
1. Android 性能優(yōu)化典范 - 第1季
2. ConstraintLayout 完全解析 快來優(yōu)化你的布局吧