多數(shù)情況下,卡頓發(fā)生的根本原因,是渲染問(wèn)題,即系統(tǒng)無(wú)法及時(shí)的完成復(fù)雜界面的渲染操作。系統(tǒng)會(huì)嘗試每個(gè)16ms對(duì)UI進(jìn)行渲染,如果每次都渲染成功,這樣畫(huà)面就是流暢的(達(dá)到了60fps);否則,就會(huì)發(fā)生丟幀現(xiàn)象,丟幀越多,用戶感受到的卡頓情況就越嚴(yán)重。
為了獲得更平滑的動(dòng)畫(huà),就必須保證幀率不低于60fps——意味著每幀只能花費(fèi)16毫秒的時(shí)間。
1、一些概念
1、刷新率vs幀率
- 刷新率:每秒屏幕刷新次數(shù),手機(jī)屏幕的刷新率是60HZ
- 幀率:GPU在一秒內(nèi)繪制的幀數(shù)
2、撕裂vs掉幀
-
撕裂
因?yàn)槠聊坏乃⑿逻^(guò)程是自上而下、自左向右的,如果幀率>刷新率,當(dāng)屏幕還沒(méi)有刷新n-1幀的數(shù)據(jù)時(shí),就開(kāi)始生成第n幀的數(shù)據(jù)了,從上到下,覆蓋第n-1幀。如果此時(shí)刷新屏幕,就會(huì)出現(xiàn)圖像的上半部分是第n幀的,下半部分是第n幀的現(xiàn)象。CPU/GPU一直都在渲染
-
丟幀
Android系統(tǒng)每隔16ms發(fā)出VSYNC信號(hào),觸發(fā)GPU對(duì)UI進(jìn)行渲染,如果你的某個(gè)操作花費(fèi)時(shí)間是24ms,系統(tǒng)在得到VSYNC信號(hào)的時(shí)候由于還沒(méi)有準(zhǔn)備好,就無(wú)法進(jìn)行更新任何內(nèi)容,那么用戶在32ms內(nèi)看到的會(huì)是同一幀畫(huà)面(卡頓現(xiàn)象),即丟幀現(xiàn)象。
3、單緩存 vs VSYNC vs 雙緩存 vs 三緩存
- 單緩存(沒(méi)有引入VSync )
GPU向緩存中寫(xiě)入數(shù)據(jù),屏幕從緩存中讀取數(shù)據(jù),刷新后顯示。由于刷新率和幀率并不總是一致的,很可能導(dǎo)致撕裂的現(xiàn)象。為了解決單緩存的畫(huà)面撕裂問(wèn)題,出現(xiàn)了雙緩存和 VSync 。
單緩存
-
VSYNC 和 雙緩存
雙緩存使用了兩個(gè)緩存區(qū): Back Buffer 、 Frame Buffer。當(dāng)寫(xiě)入下一幀時(shí),GPU會(huì)先填充 Back Buffer 中,當(dāng)刷新屏幕時(shí),屏幕從 Frame Buffer 中讀數(shù)據(jù)。VSYNC 主要是完成幀的復(fù)制,開(kāi)始下一幀的渲染。
雙緩存
當(dāng)幀率大于刷新頻率時(shí),通過(guò)使幀率被迫跟刷新頻率保持同步,從而避免畫(huà)面撕裂的現(xiàn)象(只有當(dāng) VSync 信號(hào)產(chǎn)生時(shí), CPU/GPU 才會(huì)開(kāi)始繪制)。當(dāng)VSync 信號(hào)產(chǎn)生時(shí),先完成Back Buffer 到 Frame Buffer的復(fù)制操作(通過(guò)交換內(nèi)存地址),然后通知 CPU/GPU 繪制下一幀圖像。也只有VSync 信號(hào)發(fā)生時(shí),才繪制下一幀。
當(dāng)刷新頻率>幀率時(shí),此時(shí)刷新屏幕,發(fā)出VSYNC 信號(hào),由于CPU/GPU的渲染操作還沒(méi)有完成,就不把Back Buffer的數(shù)據(jù)復(fù)制到 Frame Buffer,此時(shí)就從Frame Buffer去取舊數(shù)據(jù),這樣在兩個(gè)刷新周期里,顯示的是同一幀數(shù)據(jù), 掉幀
-
三重緩存
三緩存
雙重緩存的缺陷在于:當(dāng) CPU/GPU 繪制一幀的時(shí)間超過(guò) 16 ms 時(shí),會(huì)產(chǎn)生 Jank。更要命的是,產(chǎn)生 Jank 的那一幀的顯示期間,GPU/CPU 都是在閑置的。
如下圖,A、B 和 C 都是 Buffer。
如果有第三個(gè) Buffer 能讓 CPU/GPU 在這個(gè)時(shí)候繼續(xù)工作,那就完全可以避免第二個(gè) Jank 的發(fā)生了!
2、渲染是怎么實(shí)現(xiàn)的
如何把布局渲染成用戶可以識(shí)別的圖片?它的核心是通過(guò)柵格化操作,將一些按鈕、文字等布局對(duì)象拆分到像素點(diǎn)進(jìn)行顯示。由于柵格化是非常費(fèi)時(shí)的,因此引入了GPU加快柵格化過(guò)程。
整個(gè)處理過(guò)程是:CUP負(fù)把把UI對(duì)象轉(zhuǎn)變GPU可以識(shí)別的成圖元(多邊形、紋理),然后上傳到GPU進(jìn)行柵格化過(guò)程。
1、在GPU中保存圖元
但是由于這個(gè)轉(zhuǎn)換、上傳都是耗時(shí)的,所以為了減少時(shí)間,需要減少它們的數(shù)量,慶幸的是Open GL API,允許你將這些圖元保存到GPU,當(dāng)下次需要一個(gè)按鈕的時(shí)候,只要參考GPU中已經(jīng)存在的圖元,告訴GPU如何去繪制 它。優(yōu)化渲染性能就意味著盡可能多且快的將更多的數(shù)據(jù)上傳到GPU,然后留在GPU,盡可能長(zhǎng)時(shí)間的不去修改它。
2、display list,CPU、GPU溝通的橋梁
將圖元從CPU上傳到GPU,這個(gè)操作是在DisplayList的幫助下完成的。DisplayList持有GPU渲染時(shí)需要的信息,包括了一些圖元和Open GL命令列表。
CPU直接與GPU通信,而是通過(guò)中間的一個(gè)圖形驅(qū)動(dòng)層(Graphics Driver)來(lái)連接這兩部分。 圖形驅(qū)動(dòng)維護(hù)了一個(gè)隊(duì)列,CPU把display list添加到隊(duì)列里,GPU從這個(gè)隊(duì)列取出數(shù)據(jù)進(jìn)行繪制。
Display List,在View第一次需要被渲染時(shí)被創(chuàng)建,當(dāng)這個(gè)View要顯示到屏幕上時(shí),我們會(huì)執(zhí)行GPU的繪制指令來(lái)執(zhí)行這個(gè)Display List。
整體流程是:CUP負(fù)把把UI對(duì)象轉(zhuǎn)變GPU可以識(shí)別的成圖元(多邊形、紋理),并存儲(chǔ)進(jìn)display list列表;GPU執(zhí)行繪圖指令來(lái)執(zhí)行display list,取出相應(yīng)的圖元信息,進(jìn)行柵格化渲染
3、引起的原因和檢測(cè)方法
主要有以下幾點(diǎn),可能引起渲染的性能問(wèn)題:
- 布局Layout過(guò)于復(fù)雜,無(wú)法在16ms內(nèi)完成渲染。
- 同一時(shí)間動(dòng)畫(huà)執(zhí)行的次數(shù)過(guò)多,導(dǎo)致CPU或GPU負(fù)載過(guò)重。
- View過(guò)度繪制,導(dǎo)致某些像素在同一幀時(shí)間內(nèi)被繪制多次。
- UI線程中做了稍微耗時(shí)的操作。
可以通過(guò)如下工具進(jìn)行檢測(cè)
HierarchyViewer(布局盡量扁平化)、Show GPU Overdraw、Profile GPU Rendering等等
1、Profile GPU Rendering
1、打開(kāi)手機(jī)里面的開(kāi)發(fā)者選項(xiàng),選擇Profile GPU Rendering,選中On screen as bars的選項(xiàng)
通過(guò)Profile GPU Rendering,可以在屏幕上實(shí)時(shí)顯示渲染每一幀圖像花費(fèi)的時(shí)間。渲染時(shí)間用柱狀圖表示,每一條柱狀圖都由3部分組成,藍(lán)色、紅色和黃色,代表渲染的3個(gè)不同的階段,通過(guò)分析這三個(gè)階段的時(shí)間就可以找到渲染時(shí)的性能瓶頸。
2、使用命令
adb shell dumpsys gfxinfo yourpackagename
將得到的數(shù)據(jù)保存為txt,之后導(dǎo)出到excel(數(shù)據(jù)-導(dǎo)入數(shù)據(jù)),生成圖表
Draw:表示在Java中創(chuàng)建顯示列表部分中,OnDraw()方法占用的時(shí)間。
Process:表示渲染引擎執(zhí)行顯示列表所花的時(shí)間,view越多,時(shí)間就越長(zhǎng)
Execute:表示把一幀數(shù)據(jù)發(fā)送到屏幕上排版顯示實(shí)際花費(fèi)的時(shí)間。其實(shí)是實(shí)際顯示幀數(shù)據(jù)的后臺(tái)緩存區(qū)與前臺(tái)緩沖區(qū)交換后并將前臺(tái)緩沖區(qū)的內(nèi)容顯示到屏幕上的時(shí)間。所以這個(gè)時(shí)間,一般都很短。
Draw + Process + Execute = 完整顯示一幀 ,這個(gè)時(shí)間要小于16ms才能保存每秒60幀(即1000秒/60幀)。
將數(shù)據(jù)復(fù)制到 excel中,然后將數(shù)據(jù)生成“柱形圖”來(lái)觀察結(jié)果。
參考:Android性能專項(xiàng)FPS測(cè)試實(shí)踐
3、通過(guò)Android Studio的GPU Monitor查看
4、分析
隨著界面的刷新,界面上會(huì)滾動(dòng)顯示垂直的柱狀圖來(lái)表示每幀畫(huà)面所需要渲染的時(shí)間,柱狀圖越高表示花費(fèi)的渲染時(shí)間越長(zhǎng)。中間有一根綠色的橫線,代表16ms,我們需要確保每一幀花費(fèi)的總時(shí)間都低于這條橫線,這樣才能夠避免出現(xiàn)卡頓的問(wèn)題。
每一條柱狀線都都包含三部分
- 藍(lán)色代表測(cè)量繪制Display List的時(shí)間,是CPU將UI對(duì)象轉(zhuǎn)變成圖元,并緩存在display list的時(shí)間
視圖(display list )突然無(wú)效了 - 紅色是GPU執(zhí)行渲染指令,使用OpenGL執(zhí)行Display List的時(shí)間
view過(guò)于復(fù)雜或繪制多次 - 黃色代表CPU等待GPU處理的時(shí)
GPU做了太多的工作
總之,布局層次過(guò)深,view過(guò)于復(fù)雜都會(huì)導(dǎo)致時(shí)間過(guò)長(zhǎng)
Swap Buffers(黃色):CPU等待GPU的時(shí)間
Command Issue(紅色):OpenGL繪制display list的時(shí)間
Sync/upload(淺藍(lán)色):代表上傳bitmap到GPU的時(shí)間,如果值比較大,是因?yàn)榛ㄙM(fèi)了相當(dāng)多的時(shí)間價(jià)值大量圖片。
Draw(藍(lán)色):創(chuàng)建和更新display list的時(shí)間
Misc Time/Vsync Delay(深綠):在兩個(gè)連續(xù)的幀之間執(zhí)行的操作,如果很大,可能因?yàn)樵赨I線程做了太多的工作。
Input Handling(淺綠):處理用戶的輸入事件,花費(fèi)的時(shí)間
Animation(淺淺綠):在當(dāng)前幀中的動(dòng)畫(huà) 花費(fèi)了的時(shí)間
2、過(guò)度繪制
過(guò)度繪制(Overdraw)是屏幕上的某個(gè)像素在同一幀的時(shí)間內(nèi)被繪制了多次。可以在開(kāi)發(fā)者選項(xiàng)中打開(kāi) Show GPU Overdraw,觀察過(guò)度繪制情況。
如果沒(méi)有顏色,代表沒(méi)有過(guò)度繪制,藍(lán)色,淡綠,淡紅,深紅代表了4種不同程度的Overdraw情況,分別表示了對(duì)應(yīng)的像素被多繪制了1、2、3、4次,應(yīng)當(dāng)盡量減少紅色區(qū)域。
過(guò)度繪制可能是因?yàn)榻M件的互相重疊或者不必要的背景重疊
3、HierarchyViewer
1、打開(kāi)Hierarchy Viewer
點(diǎn)擊 Tools>Android>Android Device Monitor,選擇 Hierarchy Viewer,如下圖:
但是Hierachy Viewer默認(rèn)是無(wú)法連接真機(jī)調(diào)試,只能使用模擬器了。但是要APP先運(yùn)行起來(lái)再使用Android Device Monitor。
2、分析頁(yè)面布局性能
選中一個(gè)節(jié)點(diǎn),點(diǎn)擊 右上角,就可以獲取到布局繪制的時(shí)間,如圖
從左到右依次,代表View的Measure, Layout和Draw的性能,不同顏色代表不同的性能等級(jí):
- 綠:該View的此項(xiàng)性能比該View Tree中超過(guò)50%的View都要快
- 黃:該View的此項(xiàng)性能比該View Tree中超過(guò)50%的View都要慢
- 紅:該View的此項(xiàng)性能是View Tree中最慢的
4.2 測(cè)量結(jié)果分析
紅色節(jié)點(diǎn)是代表應(yīng)用性能慢的一個(gè)潛在問(wèn)題,下面是幾個(gè)例子,如何來(lái)分析和解釋紅點(diǎn)的出現(xiàn)原因?
1)如果在葉節(jié)點(diǎn)或者ViewGroup中,只有極少的子節(jié)點(diǎn),這可能反映出一個(gè)問(wèn)題,應(yīng)用可能在設(shè)備上運(yùn)行并不慢,但是你需要指導(dǎo)為什么這個(gè)節(jié)點(diǎn)是紅色的,可以借助Systrace或者Traceview工具,獲取更多額外的信息;
2)如果一個(gè)視圖組里面有許多的子節(jié)點(diǎn),并且測(cè)量階段呈現(xiàn)為紅色,則需要觀察下子節(jié)點(diǎn)的繪制情況;
3)如果視圖層級(jí)結(jié)構(gòu)中的根視圖,Messure階段為紅色,Layout階段為紅色,Draw階段為黃色,這個(gè)是比較常見(jiàn)的,因?yàn)檫@個(gè)節(jié)點(diǎn)是所有其它視圖的父類;
4)如果視圖結(jié)構(gòu)中的一個(gè)葉子節(jié)點(diǎn),有20個(gè)視圖是紅色的Draw階段,這是有問(wèn)題的,需要檢查代碼里面的onDraw方法,不應(yīng)該在那里調(diào)用。
- 布局常見(jiàn)問(wèn)題與優(yōu)化建議
1)沒(méi)有用的父布局時(shí)指沒(méi)有背景繪制或者沒(méi)有大小限制的父布局,這樣的布局不會(huì)對(duì)UI效果產(chǎn)生任何影響。我們可以把沒(méi)有用的父布局,通過(guò)<merge/>標(biāo)簽合并來(lái)減少UI的層次;
2)使用線性布局LinearLayout排版導(dǎo)致UI層次變深,如果有這類問(wèn)題,我們就使用相對(duì)布局RelativeLayout代替LinearLayout,減少UI的層次;
3)不常用的UI被設(shè)置成GONE,比如異常的錯(cuò)誤頁(yè)面,如果有這類問(wèn)題,我們需要用<ViewStub/>標(biāo)簽,代替GONE提高UI性能。
參考:Android 性能模式 第一季、Android性能優(yōu)化典范 - 第1季、Android性能優(yōu)化之渲染篇、Android性能優(yōu)化系列——Profile GPU Rendering、Profile GPU Rendering Walkthrough、Android 顯示原理簡(jiǎn)介、Android 4.4 Graphic系統(tǒng)詳解(2) VSYNC的生成、理解 VSync、了解Android 4.1,之三:黃油項(xiàng)目 —— 運(yùn)作機(jī)理及新鮮玩意
、Hierarchy Viewer使用詳解