一、初識(shí)布局優(yōu)化
通過《Android性能優(yōu)化(一)之啟動(dòng)加速35%》我們獲得了閃電般的App啟動(dòng)速度,那么在應(yīng)用啟動(dòng)完畢之后,UI布局也會(huì)對App的性能產(chǎn)生比較大的影響,如果布局寫得糟糕,顯而易見App的表現(xiàn)不可能流暢。
那么本文我同樣基于實(shí)際案例,針對應(yīng)用的布局進(jìn)行優(yōu)化進(jìn)而提升App性能。
二、60fps VS 16ms
根據(jù)Google官方出品的Android性能優(yōu)化典范,60幀每秒是目前最合適的圖像顯示速度,事實(shí)上絕大多數(shù)的Android設(shè)備也是按照每秒60幀來刷新的。為了讓屏幕的刷新幀率達(dá)到60fps,我們需要確保在時(shí)間16ms(1000/60Hz)內(nèi)完成單次刷新的操作(包括measure、layout以及draw),這也是Android系統(tǒng)每隔16ms就會(huì)發(fā)出一次VSYNC信號(hào)觸發(fā)對UI進(jìn)行渲染的原因。
如果整個(gè)過程在16ms內(nèi)順利完成則可以展示出流暢的畫面;然而由于任何原因?qū)е陆邮盏絍SYNC信號(hào)的時(shí)候無法完成本次刷新操作,就會(huì)產(chǎn)生掉幀的現(xiàn)象,刷新幀率自然也就跟著下降(假定刷新幀率由正常的60fps降到30fps,用戶就會(huì)明顯感知到卡頓)。
作為開發(fā)人員,我們的目標(biāo)只有一個(gè):保證穩(wěn)定的幀率來避免卡頓。
三、Avoid Overdraw
理論上一個(gè)像素每次只繪制一次是最優(yōu)的,但是由于重疊的布局導(dǎo)致一些像素會(huì)被多次繪制,Overdraw由此產(chǎn)生。
我們可以通過調(diào)試工具來檢測Overdraw:設(shè)置——開發(fā)者選項(xiàng)——調(diào)試GPU過度繪制——顯示過度繪制區(qū)域。
原色 – 沒有過度繪制 – 這部分的像素點(diǎn)只在屏幕上繪制了一次。
藍(lán)色 – 1次過度繪制– 這部分的像素點(diǎn)只在屏幕上繪制了兩次。
綠色 – 2次過度繪制 – 這部分的像素點(diǎn)只在屏幕上繪制了三次。
粉色 – 3次過度繪制 – 這部分的像素點(diǎn)只在屏幕上繪制了四次。
紅色 – 4次過度繪制 – 這部分的像素點(diǎn)只在屏幕上繪制了五次。
在實(shí)際項(xiàng)目中,一般認(rèn)為藍(lán)色即是可以接受的顏色。
我們來看一個(gè)簡單卻隱藏了很多問題的界面,App的設(shè)置界面。在沒有優(yōu)化之前打開Overdraw調(diào)試,可以看到界面大多數(shù)是嚴(yán)重的紅色:見下圖。
貼出這個(gè)布局的代碼
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F1F0F0"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@color/white"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/update_phone"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/white"
android:paddingLeft="10dp"
android:paddingRight="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:background="@color/white"
android:text="修改手機(jī)號(hào)"
android:textColor="#FF555555"
android:textSize="14sp" />
<ImageView
android:id="@+id/update_phone_iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:src="@drawable/arrow_right" />
<ImageView
android:id="@+id/update_phone_dot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginRight="10dp"
android:layout_toLeftOf="@id/update_phone_iv"
android:src="@drawable/message_logo_red" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginLeft="10dp"
android:background="#FFDDDDDD" />
<RelativeLayout
android:id="@+id/setting_lv_forgetPassword"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/white"
android:paddingLeft="10dp"
android:paddingRight="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:background="@color/white"
android:text="找回密碼"
android:textColor="#FF555555"
android:textSize="14sp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:src="@drawable/arrow_right" />
</RelativeLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@color/white"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/privacy_setting"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/white"
android:paddingLeft="10dp"
android:paddingRight="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:background="@color/white"
android:text="隱私設(shè)置"
android:textColor="#FF555555"
android:textSize="14sp" />
<ImageView
android:id="@+id/privacy_setting_iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:src="@drawable/arrow_right" />
<ImageView
android:id="@+id/privacy_setting_dot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginRight="10dp"
android:layout_toLeftOf="@id/privacy_setting_iv"
android:src="@drawable/message_logo_red" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginLeft="10dp"
android:background="#FFDDDDDD" />
<RelativeLayout
android:id="@+id/setting_lv_messageSetting"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/white"
android:paddingLeft="10dp"
android:paddingRight="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:background="@color/white"
android:text="@string/accountSetting_messageSetting"
android:textColor="#FF555555"
android:textSize="14sp" />
<CheckBox
android:id="@+id/setting_checkbox_c_messageSetting"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:checked="true" />
</RelativeLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@color/white"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/setting_lv_feedback_m"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/white"
android:paddingLeft="10dp"
android:paddingRight="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:background="@color/white"
android:text="@string/accountSetting_feedback"
android:textColor="#FF555555"
android:textSize="14sp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:src="@drawable/arrow_right" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginLeft="10dp"
android:background="#FFDDDDDD" />
<RelativeLayout
android:id="@+id/setting_lv_score"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/white"
android:paddingLeft="10dp"
android:paddingRight="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:background="@color/white"
android:text="@string/accountSetting_score"
android:textColor="#FF555555"
android:textSize="14sp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:src="@drawable/arrow_right" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginLeft="10dp"
android:background="#FFDDDDDD" />
<RelativeLayout
android:id="@+id/setting_lv_aboutus"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/white"
android:paddingLeft="10dp"
android:paddingRight="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:background="@color/white"
android:text="@string/about_us"
android:textColor="#FF555555"
android:textSize="14sp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:src="@drawable/arrow_right" />
</RelativeLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@color/white"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/setting_lv_changeStatus"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/white"
android:paddingLeft="10dp"
android:paddingRight="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:background="@color/white"
android:text="我要招人"
android:textColor="#FF555555"
android:textSize="14sp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:src="@drawable/arrow_right" />
</RelativeLayout>
</LinearLayout>
<Button
android:id="@+id/setting_btn_exitLogin"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="30dp"
android:background="@color/white"
android:gravity="center"
android:text="@string/me_exitbtn"
android:textColor="#FFFF5A5A"
android:textSize="16sp" />
</LinearLayout>
分析布局可知:多層布局重復(fù)設(shè)置了背景色導(dǎo)致Overdraw。
那么我們結(jié)合產(chǎn)品的需求(任何不結(jié)合具體場景優(yōu)化都是耍流氓):
- 去掉每行RelativeLayout的背景色;
- 去掉每行TextView的背景色;
備注:一個(gè)容易忽略的點(diǎn)是我們的Activity使用的Theme可能會(huì)默認(rèn)的加上背景色,不需要的情況下可以去掉。
去掉背景色之后再看一下Overdraw;
對比一下優(yōu)化后的布局的顏色,可以看出Overdraw降到了可以接受的程度。
備注:有些過度繪制都是不可避免的,需要結(jié)合具體的布局場景具體分析。
四、減少嵌套層次及控件個(gè)數(shù)
- Android的布局文件的加載是LayoutInflater利用pull解析方式來解析,然后根據(jù)節(jié)點(diǎn)名通過反射的方式創(chuàng)建出View對象實(shí)例;
- 同時(shí)嵌套子View的位置受父View的影響,類如RelativeLayout、LinearLayout等經(jīng)常需要measure兩次才能完成,而嵌套、相互嵌套、深層嵌套等的發(fā)生會(huì)使measure次數(shù)呈指數(shù)級(jí)增長,所費(fèi)時(shí)間呈線性增長;
由此得到結(jié)論:那么隨著控件數(shù)量越多、布局嵌套層次越深,展開布局花費(fèi)的時(shí)間幾乎是線性增長,性能也就越差。
幸運(yùn)的是,我們有Hierarchy Viewer這個(gè)方便可視化的工具,可以得到:樹形結(jié)構(gòu)總覽、布局view、每一個(gè)View(包含子View)繪制所花費(fèi)的時(shí)間及View總個(gè)數(shù)。
備注: Hierarchy Viewer不能連接真機(jī)的問題可以通過ViewServer這個(gè)庫解決;
使用Hierarchy Viewer來看查看一下設(shè)置界面,可以從下圖中得到設(shè)置界面的一些數(shù)據(jù)及存在的問題:
- 嵌套共計(jì)7層(僅setContentView設(shè)置的布局),布局嵌套過深;
- measure時(shí)間1.569ms,layout時(shí)間0.120ms,draw時(shí)間16.128ms,合計(jì)共計(jì)耗時(shí)17.871ms;
- 共繪制85個(gè)View,5個(gè)多余定位,以及若干個(gè)無用布局。
優(yōu)化方案:
- 將之前使用RelativeLayout來做的可以替換的行換為TextView;
- 去掉之前多余的無用布局;
現(xiàn)在我們再使用Hierarchy Viewer來檢測一下:
優(yōu)化后:
1. 控件數(shù)量從85個(gè)減少到26個(gè),減少69%;
2. 繪制時(shí)間從17.8ms減少到14.756ms,降低17%;
總結(jié):
1. 同樣的UI效果可以使用不同的布局來完成,我們需要考慮使用少的嵌套層次以及控件個(gè)數(shù)來完成,例如設(shè)置界面的普通一行,可以像之前一樣使用RelativeLayout嵌套TextView以及ImageView來實(shí)現(xiàn),但是明顯只使用TextView來做:嵌套層次、控件個(gè)數(shù)都更少。
2. 優(yōu)化過程中使用低端手機(jī)更易發(fā)現(xiàn)瓶頸;
五、Profiling GPU Rendering
根據(jù)Android性能優(yōu)化典范,打開設(shè)備的GPU配置渲染工具——》在屏幕上顯示為條形圖,可以協(xié)助我們定位UI渲染問題。
從Android M版本開始,GPU Profiling工具把渲染操作拆解成如下8個(gè)詳細(xì)的步驟進(jìn)行顯示。
- Swap Buffers:表示處理任務(wù)的時(shí)間,也可以說是CPU等待GPU完成任務(wù)的時(shí)間,線條越高,表示GPU做的事情越多;
- Command Issue:表示執(zhí)行任務(wù)的時(shí)間,這部分主要是Android進(jìn)行2D渲染顯示列表的時(shí)間,為了將內(nèi)容繪制到屏幕上,Android需要使用Open GL ES的API接口來繪制顯示列表,紅色線條越高表示需要繪制的視圖更多;
- Sync & Upload:表示的是準(zhǔn)備當(dāng)前界面上有待繪制的圖片所耗費(fèi)的時(shí)間,為了減少該段區(qū)域的執(zhí)行時(shí)間,我們可以減少屏幕上的圖片數(shù)量或者是縮小圖片的大??;
- Draw:表示測量和繪制視圖列表所需要的時(shí)間,藍(lán)色線條越高表示每一幀需要更新很多視圖,或者View的onDraw方法中做了耗時(shí)操作;
- Measure/Layout:表示布局的onMeasure與onLayout所花費(fèi)的時(shí)間,一旦時(shí)間過長,就需要仔細(xì)檢查自己的布局是不是存在嚴(yán)重的性能問題;
- Animation:表示計(jì)算執(zhí)行動(dòng)畫所需要花費(fèi)的時(shí)間,包含的動(dòng)畫有ObjectAnimator,ViewPropertyAnimator,Transition等等。一旦這里的執(zhí)行時(shí)間過長,就需要檢查是不是使用了非官方的動(dòng)畫工具或者是檢查動(dòng)畫執(zhí)行的過程中是不是觸發(fā)了讀寫操作等等;
- Input Handling:表示系統(tǒng)處理輸入事件所耗費(fèi)的時(shí)間,粗略等于對事件處理方法所執(zhí)行的時(shí)間。一旦執(zhí)行時(shí)間過長,意味著在處理用戶的輸入事件的地方執(zhí)行了復(fù)雜的操作;
- Misc Time/Vsync Delay:表示在主線程執(zhí)行了太多的任務(wù),導(dǎo)致UI渲染跟不上vSync的信號(hào)而出現(xiàn)掉幀的情況;出現(xiàn)該線條的時(shí)候,可以在Log中看到這樣的日志:
備注:GPU配置渲染工具雖然可以定位出問題發(fā)生在某個(gè)步驟,但是并不能定位到具體的某一行;當(dāng)我們定位到某個(gè)步驟之后可以使用工具TraceView進(jìn)行更加詳細(xì)的定位。TraceView的使用可以參照《Android性能優(yōu)化(一)之啟動(dòng)加速35%》。
六、Use Tags
merge標(biāo)簽
merge可以用來合并布局,減少布局的層級(jí)。merge多用于替換頂層FrameLayout或者include布局時(shí),用于消除因?yàn)橐貌季謱?dǎo)致的多余嵌套。
例如:需要顯示一個(gè)Button,布局如下;
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Merge標(biāo)簽演示" />
</LinearLayout>
我們通過UiAutoMatorViewer(無需root,相比Hierarchy Viewer只能查看布局層次,不能得到繪制時(shí)間)看一下布局的層次
我們使用Merge標(biāo)簽對代碼進(jìn)行修改;
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Merge標(biāo)簽演示" />
</merge>
再看下布局的層次:
可以看到使用Merge標(biāo)簽進(jìn)行優(yōu)化之后布局嵌套就少了一層,Button作為父視圖第三層FrameLayout的直接子視圖。
注意:merge標(biāo)簽常用于減少布局嵌套層次,但是只能用于根布局。
ViewStub標(biāo)簽
推遲創(chuàng)建對象、延遲初始化,不僅可以提高性能,也可以節(jié)省內(nèi)存(初始化對象不被創(chuàng)建)。Android定義了ViewStub類,ViewStub是輕量級(jí)且不可見的視圖,它沒有大小,沒有繪制功能,也不參與measure和layout,資源消耗非常低。
1、
<ViewStub
android:id="@+id/mask"
android:layout="@layout/b_me_mask"
android:layout_width="match_parent"
android:layout_height="match_parent" />
ViewStub viewStub = (ViewStub)view.findViewById(R.id.mask);
viewStub.inflate();
App里常見的視圖如蒙層、小紅點(diǎn),以及網(wǎng)絡(luò)錯(cuò)誤、沒有數(shù)據(jù)等公共視圖,使用頻率并不高,如果每一次都參與繪制其實(shí)是浪費(fèi)資源的,都可以借助ViewStub標(biāo)簽進(jìn)行延遲初始化,僅當(dāng)使用時(shí)才去初始化。
include標(biāo)簽
include標(biāo)簽和布局性能關(guān)系不大,主要用于布局重用,一般和merge標(biāo)簽配合使用,因和本文主題關(guān)聯(lián)不大,此處不展開討論。
七、其它
- 自定義控件時(shí),注意在onDraw不能進(jìn)行復(fù)雜運(yùn)算;以及對待三方UI庫選擇高性能;
- 內(nèi)存對布局的影響:如同Misc Time/Vsync Delay步驟產(chǎn)生的影響,在之后內(nèi)存優(yōu)化的篇章詳細(xì)講。
八、總結(jié)
布局優(yōu)化的通用套路
- 調(diào)試GPU過度繪制,將Overdraw降低到合理范圍內(nèi);
- 減少嵌套層次及控件個(gè)數(shù),保持view的樹形結(jié)構(gòu)盡量扁平(使用Hierarchy Viewer可以方便的查看),同時(shí)移除所有不需要渲染的view;
- 使用GPU配置渲染工具,定位出問題發(fā)生在具體哪個(gè)步驟,使用TraceView精準(zhǔn)定位代碼;
- 使用標(biāo)簽,Merge減少嵌套層次、ViewStub延遲初始化。
經(jīng)過這幾步的優(yōu)化之后,一般就不會(huì)再有布局的性能問題,同時(shí)還是要強(qiáng)調(diào):優(yōu)化是一個(gè)長期的工作,同時(shí)也必須結(jié)合具體場景:有取有舍!
歡迎關(guān)注微信公眾號(hào):定期分享Java、Android干貨!