本片是對Android的性能優(yōu)化的一系列文章中的其中一篇的翻譯,原文地址如下
https://developer.android.com/training/improving-layouts/optimizing-layout.html#Inspect
一.前言
布局是Android應(yīng)用直接影響用戶體驗(yàn)的一個(gè)重要的部分,如果優(yōu)化的不好,那么應(yīng)用很可能頻繁的出現(xiàn)內(nèi)存不足以及界面響應(yīng)過慢的問題。Android的SDK已經(jīng)提供了一系列的工具用于幫助開發(fā)者找出布局中的一些問題,這里會(huì)敘述一系列的案例并結(jié)合這些工具的使用來幫助開發(fā)者實(shí)現(xiàn)一個(gè)流暢的布局界面。
本片文章將從下面幾個(gè)部分進(jìn)行敘述
- 優(yōu)化布局層級
正如我們?nèi)粘J褂玫臑g覽器一樣,一個(gè)復(fù)雜的web頁面會(huì)讓加載時(shí)間邊長,我們的布局層級如果特別復(fù)雜同樣也會(huì)導(dǎo)致性能問題。本篇案例闡釋了如何使用SDK的工具檢查應(yīng)用的布局問題以及發(fā)現(xiàn)性能瓶頸。 - 利用<include/>復(fù)用布局
應(yīng)用中的UI重復(fù)地在多處使用,就需要結(jié)合本篇案例了解如何創(chuàng)建一個(gè)高效的可重用的布局,并且在需要的地方include這些UI。 - 懶加載View
你可能希望只在需要的時(shí)候才讓被包含的布局課件,比如當(dāng)Activity正在運(yùn)行的時(shí)候。這篇案例介紹了如何通過只在需要的時(shí)候加載視圖來提升布局的初始化性能。 - 讓ListView起飛
如果你曾經(jīng)創(chuàng)建過一個(gè)包含復(fù)雜列表項(xiàng)的ListView實(shí)例,那么這個(gè)列表的滾動(dòng)性能會(huì)讓你抓狂。這篇案例提供了一些小的幫助點(diǎn)用于幫助大家創(chuàng)建一個(gè)流暢的ListView
二.優(yōu)化布局層級
使用SDK提供的基礎(chǔ)Layout控件就一定能構(gòu)建出一個(gè)高效的布局結(jié)構(gòu)是一個(gè)常見的誤區(qū),實(shí)際上我們布局中使用的所有控件在運(yùn)行中都是需要經(jīng)歷初始化、布局、繪制三個(gè)步驟的。那么使用嵌套的LinearLayout會(huì)產(chǎn)生非常深的視圖層級,三個(gè)步驟也就被反復(fù)執(zhí)行,更有如果在這些嵌套的LinearLayout中還使用layout_weight參數(shù)的話更容易造成性能問題,因?yàn)槊恳粋€(gè)子布局都需要測量(Measure)兩次。這一點(diǎn)是非常重要的,尤其是在一些經(jīng)常被復(fù)用的布局視圖如ListView或者GridView中。
下面將通過例子的方式展示Hierarchy Viewer以及Layoutopt的使用
檢查布局
Android SDK中有一個(gè)叫做『Hierarchy Viewer』的工具,它能夠幫助你在應(yīng)用運(yùn)行的過程中去分析應(yīng)用的布局,以便去發(fā)現(xiàn)布局性能的瓶頸。
Hierarchy Viewer的使用非常簡單,首先在模擬器或者已經(jīng)連接上的真機(jī)中選擇需要分析的進(jìn)程,然后就可以看到布局樹(Layout Tree)了。布局樹中每一塊都用紅黃綠三色信號燈描述當(dāng)前的測量、布局、繪制的性能,用于幫助你發(fā)現(xiàn)一些潛在的問題。
比如Figure 1展示了一個(gè)簡單的布局,這個(gè)布局由左側(cè)的Bitmap以及右側(cè)豎直排序的兩個(gè)TextView構(gòu)成。這是一個(gè)用于ListView的Item的經(jīng)典結(jié)構(gòu),因此這一布局的性能非常重要,因?yàn)樗鼘⒈粡?fù)用很多次,同時(shí)它的優(yōu)化帶來的性能提升也非常大。

Hierarchy Viewer在SDK目錄下的tools目錄下,打開之后會(huì)發(fā)現(xiàn)它展示了所有鏈接上的設(shè)備以及在它們之上運(yùn)行的一些項(xiàng)目。點(diǎn)擊Load View Hierarcy可以去查看選中的項(xiàng)目(注:我這邊貼一張圖作為補(bǔ)充,如下所示,可以看到我這里鏈接了一款樂視的手機(jī),其中可以選擇進(jìn)行分析的項(xiàng)目有多個(gè))

點(diǎn)擊Load View Hierarcy之后會(huì)出現(xiàn)Figure 2所示的實(shí)際結(jié)果

在Figure 2中我們可以看到這里有三層視圖結(jié)構(gòu),其中在展示文本項(xiàng)的時(shí)候似乎出現(xiàn)了一點(diǎn)問題。點(diǎn)擊其中的每一個(gè)小塊可以看到測量,布局,繪制的詳細(xì)耗時(shí)情況,如Figure 3所示,這樣就可以有針對性的對某一部分進(jìn)行優(yōu)化。

因此ListView中每一項(xiàng)渲染的實(shí)際耗時(shí)情況如下
- 測量: 0.977ms
- 布局: 0.167ms
- 繪制: 2.717ms
修正布局
通過Figure 2我們可以看到嵌套的LinearLayout造成了布局上的性能問題,通過將嵌套的布局拆開保證布局樹的扁平化是優(yōu)化的一個(gè)方向。RelativeLayout作為根節(jié)點(diǎn)可以達(dá)到我們的目的,實(shí)際上使用RelativeLayout之后我們原有的視圖層級從3層降到了2層,分析結(jié)果也變成Figure 4所示的樣子

此時(shí),渲染ListView的一項(xiàng)耗時(shí)情況如下
- 測量: 0.598ms
- 布局: 0.110ms
- 繪制: 2.146ms
看起來是很小的提升,但是考慮到ListView中Item的復(fù)用性,這一點(diǎn)提升也不容小覷。
這些時(shí)間上的區(qū)別更多的還是由于在LinearLayout中使用了layout_weight參數(shù)導(dǎo)致的,這個(gè)參數(shù)會(huì)讓測量部分耗時(shí)翻倍。上面只是一個(gè)簡單的小例子,實(shí)際使用中我們應(yīng)該根據(jù)需要更恰當(dāng)?shù)倪x擇不同的布局。
使用Lint
在布局文件上使用lint工具用于發(fā)現(xiàn)布局上可以優(yōu)化的點(diǎn)是一個(gè)非常好的習(xí)慣。lint由于具有非常強(qiáng)大的功能,因此替代了以前使用的Layoutopt工具,一些簡單的lint規(guī)范案例如下
- 使用compound drawable
僅包含TextView和ImageView的LinearLayout布局可以使用compound drawable高效的替換。 - 合并根Frame
如果一個(gè)FrameLayout作為根節(jié)點(diǎn)但是沒有包含背景顏色同時(shí)也不包含任何padding等信息,那么可以使用merge標(biāo)簽替換,這樣可以略微提升性能。 - 無用葉視圖
如果一個(gè)布局沒有任何子視圖同時(shí)也不包含背景,那么它往往可以被移除(因?yàn)樗静豢梢姡?,這樣可以有助于降低整個(gè)視圖的層級,從而提升布局性能。 - 無用父容器
如果一個(gè)布局僅包含一個(gè)子視圖,并且自身不是ScrollView也不是根布局同時(shí)也不包含背景的話,那么它也可以被移除,同時(shí)將自己的子視圖作為自己父視圖的直接子視圖,這樣也有助于降低整個(gè)視圖的層級。 - 過深布局
嵌套層級過大的布局性能往往非常差,有時(shí)候我們應(yīng)該考慮使用RelativeLayout或者GridLayout來幫助我們降低視圖層級,默認(rèn)的最大深度是10(注:指的是默認(rèn)的lint檢查會(huì)在單一布局文件布局深度到10的時(shí)候出現(xiàn)警告)。
使用lint的另一個(gè)好處是它已經(jīng)集成到Android Studio中了,lint將在應(yīng)用編譯的時(shí)候自動(dòng)運(yùn)行。使用Android Studio你可以在構(gòu)建某一特定的變種版本(Build variant)或者全部變種版本的時(shí)候執(zhí)行l(wèi)int檢查。
你可以自己配置lint的檢查文件去自定義一些內(nèi)容,入口在Android Studio的File>Settings>Project Settings中,如Figure 5所示

lint可以對代碼提供一些建議,同時(shí)也能幫我們自動(dòng)修復(fù)一些問題。


項(xiàng)目問題
HV工具可以很好的幫助我們發(fā)現(xiàn)布局中的一些問題,具體使用可以參考Optimizing Your UI,同時(shí)lint的能力不僅僅體現(xiàn)在布局優(yōu)化上,想要運(yùn)行l(wèi)int,也可以直接點(diǎn)擊Analyze>Inspect Code,最終結(jié)果會(huì)分類目詳細(xì)展示出來。具體如何使用lint,可以參考Improve Your Code with Lint
三.利用<include/>復(fù)用布局
雖然Android提供了很多的控件用于幫助我們在在布局文件中進(jìn)行元素復(fù)用,但是實(shí)際項(xiàng)目中我們也許還需要更大層面上的復(fù)用元素,比如一個(gè)特殊的布局。為了高效的復(fù)用整個(gè)布局,你可以使用<include/>和<merge/>標(biāo)簽將已有布局嵌入其他布局中。
使用這個(gè)能力可以讓你創(chuàng)造出非常復(fù)雜的可復(fù)用布局,比如一個(gè)帶有yes/no的按鈕板,帶有文字描述的進(jìn)度條。這同樣意為著你項(xiàng)目中的任何一個(gè)布局元素都可以分開進(jìn)行管理,當(dāng)需要的時(shí)候你只要用到include就行。
創(chuàng)造可復(fù)用布局
如果你已經(jīng)知道哪些布局需要被重用,那只需要新建一個(gè)xml文件,然后將被重用的布局寫入進(jìn)去就可以了。比如下面就是一個(gè)可以被重用的布局,文件名為titlebar.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/titlebar_bg">
<ImageView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/gafricalogo" />
</FrameLayout>
使用<include>標(biāo)簽
在你需要的地方用<include />標(biāo)簽添加之前定義的布局即可重用布局。比如我需要重用上面定義的titlebar.xml布局,代碼可以這樣
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/app_bg"
android:gravity="center_horizontal">
<include layout="@layout/titlebar"/>
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hello"
android:padding="10dp" />
...
</LinearLayout>
include中所有l(wèi)ayout開頭的屬性(android:layout_*)都可以被重寫,但他們只有在重寫了android:layout_height和android:layout_width之后才生效
使用<merge>標(biāo)簽
merge標(biāo)簽可以幫助我們剔除布局層級中無用的視圖層。比如你有一個(gè)LinearLayout作為根視圖的布局,它里面需要有一個(gè)包含兩個(gè)連續(xù)視圖(比如按鈕)的可重用布局,這個(gè)可重用布局你需要重新定義它的根視圖,比如你會(huì)使用LinearLayout。但是這時(shí)候使用該LinearLayout作為可重用布局的根視圖會(huì)導(dǎo)致增加一個(gè)毫無用處的視圖層級。為了避免這種現(xiàn)象,可以使用<merge>作為可重用布局的根視圖,比如
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/add"/>
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/delete"/>
</merge>
此時(shí)如果你將這個(gè)可重用布局使用<include>標(biāo)簽包含至其他布局文件中,系統(tǒng)會(huì)忽略merge元素然后將兩個(gè)Button直接放置在include標(biāo)簽所在的地方。
注意點(diǎn)
<include>
- 重寫layout_*的屬性記得先重寫android:layout_height和android:layout_width。
- include如果指定了id,那么layout屬性的根視圖id會(huì)被強(qiáng)制修改成include中的id,如果不注意很容易出現(xiàn)空指針問題。驗(yàn)證起來很簡單,只需要使用HV工具即可,實(shí)際的源碼分析就不貼出來了,有興趣的話可以參考LayoutInflater.inflate方法
<merge>
- 上一節(jié)提到的布局文件,復(fù)用在LinearLayout和RelativeLayout中會(huì)有不同的表現(xiàn),在前者會(huì)以線性的方式布局,后者delete按鈕會(huì)遮擋add按鈕,所以使用merge標(biāo)簽一定要注意實(shí)際的根視圖類型
-
merge必須放在布局文件的根節(jié)點(diǎn)上
- merge并不是一個(gè)ViewGroup,也不是一個(gè)View,它相當(dāng)于聲明了一些視圖,等待被添加。
-
因?yàn)閙erge標(biāo)簽并不是View,所以在通過LayoutInflate.inflate方法渲染的時(shí)候, 第二個(gè)參數(shù)必須指定一個(gè)父容器,且第三個(gè)參數(shù)必須為true,也就是必須為merge下的視圖指定一個(gè)父親節(jié)點(diǎn)。
- 因?yàn)閙erge不是View,所以對merge標(biāo)簽設(shè)置的所有屬性都是無效的
- 如果Activity的布局文件根節(jié)點(diǎn)是FrameLayout,可以替換為merge標(biāo)簽,這樣,執(zhí)行setContentView之后,會(huì)減少一層FrameLayout節(jié)點(diǎn)。
關(guān)于Activity根節(jié)點(diǎn)是FrameLayout的證據(jù),使用HV工具可以得到。 - 自定義XXXLayout控件時(shí),如果使用LayoutInflater.inflate(R.layout.xxx, this, true)填充視圖,那么該布局的根元素最好設(shè)置成<merge>,這一點(diǎn)其實(shí)是和上一點(diǎn)相同的,有助于直接減少視圖層級。
項(xiàng)目問題
項(xiàng)目中使用include的地方有四十多處,大家對這個(gè)標(biāo)簽其實(shí)也比較屬性,相反merge的話使用的地方僅有一處,建議可以結(jié)合上面的注意點(diǎn)嘗試使用。
四.懶加載View
你的布局中可能存在很少情況下才用到的復(fù)雜布局,比如單條詳情、進(jìn)圖條或者是一些撤銷消息等等,這些布局可以只在你需要的時(shí)候才加載以提升布局的渲染速度。
定義ViewStub
ViewStub是一個(gè)輕量級的視圖,它不參與繪制也不參與任何的布局工作。因此,它在視圖層級的構(gòu)建中消耗的資源是非常小的。每一個(gè)ViewStub在使用時(shí)只需要通過android:layout去定義它需要加載布局文件即可。
下面給出的ViewStub承載了一個(gè)透明的進(jìn)度條,它只在特定情況下才需要展現(xiàn)給用戶。
<ViewStub
android:id="@+id/stub_import"
android:inflatedId="@+id/panel_import"
android:layout="@layout/progress_overlay"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />
加載ViewStub布局
當(dāng)我們需要讓ViewStub承載的視圖展現(xiàn)時(shí),只需要通過調(diào)用setVisibility(View.VISIBLE)或者inflate()方法即可。
((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
// or
View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();
一旦ViewStub被可見或者被布局了,那么它就從視圖層級中剝離出來,取代ViewStub存在于視圖層級的是android:layout屬性所指定的布局,該布局的id可以通過android:inflatedId指定。


這里和include一樣,android:inflatedId屬性也會(huì)覆蓋layout中根視圖的id。
注意點(diǎn)
ViewStub是一個(gè)比較特殊的View,與渲染相關(guān)的方法它的實(shí)現(xiàn)基本都是空實(shí)現(xiàn),因此能節(jié)約很多性能。除此之外它的其他特性要求我們使用時(shí)要稍加注意。
- ViewStub只能被inflate一次,多次調(diào)用會(huì)出異常。第一次setVisibility(View.Visibility)會(huì)被動(dòng)調(diào)用一次inflate,因此需要注意。
- ViewStub被inflate之后會(huì)從視圖層級中移除,因此再次調(diào)用findViewById嘗試獲取ViewStub對象會(huì)返回空,不要嘗試使用該對象,否則會(huì)出現(xiàn)空指針。
- ViewStub中l(wèi)ayout_*屬性都是為新加載的視圖的根視圖設(shè)置的,與<include>標(biāo)簽一樣,ViewStub加載的根視圖自身的layout_*屬性會(huì)被ViewStub重寫。比如layout_height,它不能指定ViewStub本身的高度,因?yàn)閂iewStub本身的高度和寬度都是0,它指定的其實(shí)是需要加載的布局的根視圖高度。又由于此,在布局時(shí)要注意基于ViewStub的相對布局在ViewStub未inflate之前,位置與實(shí)際位置是有偏差的。
- 一般xml文件中定義的屬性都可以通過代碼設(shè)置,同樣ViewStub也可以通過方法setLayoutResource在代碼中動(dòng)態(tài)設(shè)置應(yīng)該加載的layout文件,此時(shí)一個(gè)ViewStub就可以根據(jù)邏輯不同使用不同的視圖。
項(xiàng)目問題
項(xiàng)目中可能大家更習(xí)慣使用Visibility的切換而不是ViewStub。如果在布局中你有需要設(shè)置可見性的地方,不妨思考是否需要頻繁切換它的可見狀態(tài),是否需要懶加載,如果用戶見到的可能性不大或者它本身也不經(jīng)常切換自身的可見性,那么可以考慮使用ViewStub,比如『點(diǎn)擊展開詳情』這種類似的功能。
五.讓ListView起飛
讓ListView更流暢的最重要的一點(diǎn)是要牢記讓主線程從繁雜的任務(wù)任務(wù)中解放出來,確保磁盤訪問、網(wǎng)絡(luò)請求或者數(shù)據(jù)庫操作是在單獨(dú)的線程中的。可以使用StrictMode去驗(yàn)證你的app是否遵循這一點(diǎn)。
使用后臺(tái)線程
使用后臺(tái)線程(也成為工作線程)去處理原本打算放在主線程中的復(fù)雜邏輯,以保證主線程更專注的處理UI繪制工作。通常情況下,使用AsyncTask提供了一個(gè)非常簡單的方式用于幫助你在主線程之外處理邏輯。AsyncTask自動(dòng)的將所有的execute()請求隊(duì)列化,然后依次執(zhí)行,當(dāng)然這一策略不會(huì)影響你自己創(chuàng)建的線程池。
在下面的樣例代碼中,AsyncTask用于在后臺(tái)加載圖片,并且在圖片加載完成后提供給主線程使用,它允許在圖片加載過程中使用進(jìn)度條做界面展示。
// Using an AsyncTask to load the slow images in a background thread
new AsyncTask<ViewHolder, Void, Bitmap>() {
private ViewHolder v;
@Override
protected Bitmap doInBackground(ViewHolder... params) {
v = params[0];
return mFakeImageLoader.getImage();
}
@Override
protected void onPostExecute(Bitmap result) {
super.onPostExecute(result);
if (v.position == position) {
// If this item hasn't been recycled already, hide the
// progress and set and show the image
v.progress.setVisibility(View.GONE);
v.icon.setVisibility(View.VISIBLE);
v.icon.setImageBitmap(result);
}
}
}.execute(holder);
從3.0開始,AsyncTask提供了額外的方式以允許你提高在多核手機(jī)上的并發(fā)處理能力,此時(shí)你需要用executeOnExecutor()替換之前的execute()方法。(注:AsyncTask在剛開始是以單獨(dú)線程去處理所有請求的,從1.6開始被修改成以線程池的方式處理所有的請求,但是從3.0開始又改成了單獨(dú)線程處理所有請求,想想谷歌是挺好玩的。不過正如這里說3.0版本之后你可以使用executeOnExecutor方法去制定自己的AsyncTask線程池。)
使用View Holder保持視圖對象
你可能要使用findViewById()方法去獲取視圖對象,但是如果getView時(shí)也這么做的話,那么滾動(dòng)的過程中就會(huì)觸發(fā)N多次該方法的調(diào)用,這一點(diǎn)即便在Adapter提供了滾動(dòng)過程中使用復(fù)用視圖以避免重復(fù)inflate也無法得到改觀。一個(gè)比較好的方法去避免N多次調(diào)用findViewById是使用『view holder』。
一個(gè)ViewHolder對象存儲(chǔ)了ListView中的Item的Layout中所需要的視圖組件,所以使用了ViewHolder之后你可以直接訪問到它們而無需多次調(diào)用findViewById。為了使用ViewHolder首先你需要定義自己的類
static class ViewHolder {
TextView text;
TextView timestamp;
ImageView icon;
ProgressBar progress;
int position;
}
然后在需要的時(shí)候創(chuàng)建并存儲(chǔ)它
ViewHolder holder = new ViewHolder();
holder.icon = (ImageView) convertView.findViewById(R.id.listitem_image);
holder.text = (TextView) convertView.findViewById(R.id.listitem_text);
holder.timestamp = (TextView) convertView.findViewById(R.id.listitem_timestamp);
holder.progress = (ProgressBar) convertView.findViewById(R.id.progress_spinner);
convertView.setTag(holder);
現(xiàn)在你就可以直接訪問到所有的視圖了,節(jié)約了很多資源。
項(xiàng)目問題
后臺(tái)線程還有ViewHolder,大家使用的已經(jīng)很多了,應(yīng)該沒有特別大的問題。

