本片是對Android的性能優(yōu)化的一系列文章中的其中一篇的翻譯,原文地址如下
https://developer.android.com/training/improving-layouts/optimizing-layout.html#Inspect
一.前言
布局是Android應用直接影響用戶體驗的一個重要的部分抗蠢,如果優(yōu)化的不好犬庇,那么應用很可能頻繁的出現(xiàn)內存不足以及界面響應過慢的問題。Android的SDK已經(jīng)提供了一系列的工具用于幫助開發(fā)者找出布局中的一些問題呆细,這里會敘述一系列的案例并結合這些工具的使用來幫助開發(fā)者實現(xiàn)一個流暢的布局界面型宝。
本片文章將從下面幾個部分進行敘述
- 優(yōu)化布局層級
正如我們日常使用的瀏覽器一樣,一個復雜的web頁面會讓加載時間邊長絮爷,我們的布局層級如果特別復雜同樣也會導致性能問題趴酣。本篇案例闡釋了如何使用SDK的工具檢查應用的布局問題以及發(fā)現(xiàn)性能瓶頸。 - 利用<include/>復用布局
應用中的UI重復地在多處使用坑夯,就需要結合本篇案例了解如何創(chuàng)建一個高效的可重用的布局镰惦,并且在需要的地方include這些UI巧号。 - 懶加載View
你可能希望只在需要的時候才讓被包含的布局課件,比如當Activity正在運行的時候。這篇案例介紹了如何通過只在需要的時候加載視圖來提升布局的初始化性能圣絮。 - 讓ListView起飛
如果你曾經(jīng)創(chuàng)建過一個包含復雜列表項的ListView實例蚤认,那么這個列表的滾動性能會讓你抓狂稳摄。這篇案例提供了一些小的幫助點用于幫助大家創(chuàng)建一個流暢的ListView
二.優(yōu)化布局層級
使用SDK提供的基礎Layout控件就一定能構建出一個高效的布局結構是一個常見的誤區(qū)室囊,實際上我們布局中使用的所有控件在運行中都是需要經(jīng)歷初始化、布局秘噪、繪制三個步驟的狸吞。那么使用嵌套的LinearLayout會產(chǎn)生非常深的視圖層級,三個步驟也就被反復執(zhí)行指煎,更有如果在這些嵌套的LinearLayout中還使用layout_weight參數(shù)的話更容易造成性能問題蹋偏,因為每一個子布局都需要測量(Measure)兩次。這一點是非常重要的贯要,尤其是在一些經(jīng)常被復用的布局視圖如ListView或者GridView中暖侨。
下面將通過例子的方式展示Hierarchy Viewer以及Layoutopt的使用
檢查布局
Android SDK中有一個叫做『Hierarchy Viewer』的工具,它能夠幫助你在應用運行的過程中去分析應用的布局崇渗,以便去發(fā)現(xiàn)布局性能的瓶頸字逗。
Hierarchy Viewer的使用非常簡單,首先在模擬器或者已經(jīng)連接上的真機中選擇需要分析的進程宅广,然后就可以看到布局樹(Layout Tree)了葫掉。布局樹中每一塊都用紅黃綠三色信號燈描述當前的測量、布局跟狱、繪制的性能俭厚,用于幫助你發(fā)現(xiàn)一些潛在的問題。
比如Figure 1展示了一個簡單的布局驶臊,這個布局由左側的Bitmap以及右側豎直排序的兩個TextView構成挪挤。這是一個用于ListView的Item的經(jīng)典結構叼丑,因此這一布局的性能非常重要,因為它將被復用很多次扛门,同時它的優(yōu)化帶來的性能提升也非常大鸠信。
Hierarchy Viewer在SDK目錄下的tools目錄下,打開之后會發(fā)現(xiàn)它展示了所有鏈接上的設備以及在它們之上運行的一些項目论寨。點擊Load View Hierarcy可以去查看選中的項目(注:我這邊貼一張圖作為補充星立,如下所示,可以看到我這里鏈接了一款樂視的手機葬凳,其中可以選擇進行分析的項目有多個)
點擊Load View Hierarcy之后會出現(xiàn)Figure 2所示的實際結果
在Figure 2中我們可以看到這里有三層視圖結構绰垂,其中在展示文本項的時候似乎出現(xiàn)了一點問題。點擊其中的每一個小塊可以看到測量火焰,布局劲装,繪制的詳細耗時情況,如Figure 3所示荐健,這樣就可以有針對性的對某一部分進行優(yōu)化酱畅。
因此ListView中每一項渲染的實際耗時情況如下
- 測量: 0.977ms
- 布局: 0.167ms
- 繪制: 2.717ms
修正布局
通過Figure 2我們可以看到嵌套的LinearLayout造成了布局上的性能問題,通過將嵌套的布局拆開保證布局樹的扁平化是優(yōu)化的一個方向江场。RelativeLayout作為根節(jié)點可以達到我們的目的,實際上使用RelativeLayout之后我們原有的視圖層級從3層降到了2層窖逗,分析結果也變成Figure 4所示的樣子
此時址否,渲染ListView的一項耗時情況如下
- 測量: 0.598ms
- 布局: 0.110ms
- 繪制: 2.146ms
看起來是很小的提升,但是考慮到ListView中Item的復用性碎紊,這一點提升也不容小覷佑附。
這些時間上的區(qū)別更多的還是由于在LinearLayout中使用了layout_weight參數(shù)導致的,這個參數(shù)會讓測量部分耗時翻倍仗考。上面只是一個簡單的小例子音同,實際使用中我們應該根據(jù)需要更恰當?shù)倪x擇不同的布局。
使用Lint
在布局文件上使用lint工具用于發(fā)現(xiàn)布局上可以優(yōu)化的點是一個非常好的習慣秃嗜。lint由于具有非常強大的功能权均,因此替代了以前使用的Layoutopt工具,一些簡單的lint規(guī)范案例如下
- 使用compound drawable
僅包含TextView和ImageView的LinearLayout布局可以使用compound drawable高效的替換锅锨。 - 合并根Frame
如果一個FrameLayout作為根節(jié)點但是沒有包含背景顏色同時也不包含任何padding等信息叽赊,那么可以使用merge標簽替換,這樣可以略微提升性能必搞。 - 無用葉視圖
如果一個布局沒有任何子視圖同時也不包含背景必指,那么它往往可以被移除(因為它根本不可見),這樣可以有助于降低整個視圖的層級恕洲,從而提升布局性能塔橡。 - 無用父容器
如果一個布局僅包含一個子視圖梅割,并且自身不是ScrollView也不是根布局同時也不包含背景的話,那么它也可以被移除葛家,同時將自己的子視圖作為自己父視圖的直接子視圖炮捧,這樣也有助于降低整個視圖的層級。 - 過深布局
嵌套層級過大的布局性能往往非常差惦银,有時候我們應該考慮使用RelativeLayout或者GridLayout來幫助我們降低視圖層級咆课,默認的最大深度是10(注:指的是默認的lint檢查會在單一布局文件布局深度到10的時候出現(xiàn)警告)。
使用lint的另一個好處是它已經(jīng)集成到Android Studio中了扯俱,lint將在應用編譯的時候自動運行书蚪。使用Android Studio你可以在構建某一特定的變種版本(Build variant)或者全部變種版本的時候執(zhí)行l(wèi)int檢查。
你可以自己配置lint的檢查文件去自定義一些內容迅栅,入口在Android Studio的File>Settings>Project Settings中殊校,如Figure 5所示
lint可以對代碼提供一些建議,同時也能幫我們自動修復一些問題读存。
項目問題
HV工具可以很好的幫助我們發(fā)現(xiàn)布局中的一些問題为流,具體使用可以參考Optimizing Your UI,同時lint的能力不僅僅體現(xiàn)在布局優(yōu)化上让簿,想要運行l(wèi)int敬察,也可以直接點擊Analyze>Inspect Code,最終結果會分類目詳細展示出來尔当。具體如何使用lint莲祸,可以參考Improve Your Code with Lint
三.利用<include/>復用布局
雖然Android提供了很多的控件用于幫助我們在在布局文件中進行元素復用,但是實際項目中我們也許還需要更大層面上的復用元素椭迎,比如一個特殊的布局锐帜。為了高效的復用整個布局,你可以使用<include/>和<merge/>標簽將已有布局嵌入其他布局中畜号。
使用這個能力可以讓你創(chuàng)造出非常復雜的可復用布局缴阎,比如一個帶有yes/no的按鈕板,帶有文字描述的進度條简软。這同樣意為著你項目中的任何一個布局元素都可以分開進行管理蛮拔,當需要的時候你只要用到include就行。
創(chuàng)造可復用布局
如果你已經(jīng)知道哪些布局需要被重用替饿,那只需要新建一個xml文件语泽,然后將被重用的布局寫入進去就可以了。比如下面就是一個可以被重用的布局视卢,文件名為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>標簽
在你需要的地方用<include />標簽添加之前定義的布局即可重用布局踱卵。比如我需要重用上面定義的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>標簽
merge標簽可以幫助我們剔除布局層級中無用的視圖層惋砂。比如你有一個LinearLayout作為根視圖的布局妒挎,它里面需要有一個包含兩個連續(xù)視圖(比如按鈕)的可重用布局,這個可重用布局你需要重新定義它的根視圖西饵,比如你會使用LinearLayout酝掩。但是這時候使用該LinearLayout作為可重用布局的根視圖會導致增加一個毫無用處的視圖層級。為了避免這種現(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>
此時如果你將這個可重用布局使用<include>標簽包含至其他布局文件中,系統(tǒng)會忽略merge元素然后將兩個Button直接放置在include標簽所在的地方驯嘱。
注意點
<include>
- 重寫layout_*的屬性記得先重寫android:layout_height和android:layout_width镶苞。
- include如果指定了id,那么layout屬性的根視圖id會被強制修改成include中的id鞠评,如果不注意很容易出現(xiàn)空指針問題茂蚓。驗證起來很簡單,只需要使用HV工具即可剃幌,實際的源碼分析就不貼出來了聋涨,有興趣的話可以參考LayoutInflater.inflate方法
<merge>
- 上一節(jié)提到的布局文件,復用在LinearLayout和RelativeLayout中會有不同的表現(xiàn)负乡,在前者會以線性的方式布局牍白,后者delete按鈕會遮擋add按鈕,所以使用merge標簽一定要注意實際的根視圖類型
-
merge必須放在布局文件的根節(jié)點上
- merge并不是一個ViewGroup敬鬓,也不是一個View淹朋,它相當于聲明了一些視圖,等待被添加钉答。
-
因為merge標簽并不是View,所以在通過LayoutInflate.inflate方法渲染的時候杈抢, 第二個參數(shù)必須指定一個父容器数尿,且第三個參數(shù)必須為true,也就是必須為merge下的視圖指定一個父親節(jié)點惶楼。
- 因為merge不是View右蹦,所以對merge標簽設置的所有屬性都是無效的
- 如果Activity的布局文件根節(jié)點是FrameLayout,可以替換為merge標簽歼捐,這樣何陆,執(zhí)行setContentView之后,會減少一層FrameLayout節(jié)點豹储。
關于Activity根節(jié)點是FrameLayout的證據(jù)贷盲,使用HV工具可以得到。 - 自定義XXXLayout控件時,如果使用LayoutInflater.inflate(R.layout.xxx, this, true)填充視圖巩剖,那么該布局的根元素最好設置成<merge>铝穷,這一點其實是和上一點相同的,有助于直接減少視圖層級佳魔。
項目問題
項目中使用include的地方有四十多處曙聂,大家對這個標簽其實也比較屬性,相反merge的話使用的地方僅有一處鞠鲜,建議可以結合上面的注意點嘗試使用宁脊。
四.懶加載View
你的布局中可能存在很少情況下才用到的復雜布局,比如單條詳情贤姆、進圖條或者是一些撤銷消息等等榆苞,這些布局可以只在你需要的時候才加載以提升布局的渲染速度。
定義ViewStub
ViewStub是一個輕量級的視圖庐氮,它不參與繪制也不參與任何的布局工作语稠。因此,它在視圖層級的構建中消耗的資源是非常小的弄砍。每一個ViewStub在使用時只需要通過android:layout去定義它需要加載布局文件即可仙畦。
下面給出的ViewStub承載了一個透明的進度條,它只在特定情況下才需要展現(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布局
當我們需要讓ViewStub承載的視圖展現(xiàn)時慨畸,只需要通過調用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屬性也會覆蓋layout中根視圖的id。
注意點
ViewStub是一個比較特殊的View住册,與渲染相關的方法它的實現(xiàn)基本都是空實現(xiàn)婶博,因此能節(jié)約很多性能。除此之外它的其他特性要求我們使用時要稍加注意荧飞。
- ViewStub只能被inflate一次凡人,多次調用會出異常。第一次setVisibility(View.Visibility)會被動調用一次inflate叹阔,因此需要注意挠轴。
- ViewStub被inflate之后會從視圖層級中移除,因此再次調用findViewById嘗試獲取ViewStub對象會返回空耳幢,不要嘗試使用該對象岸晦,否則會出現(xiàn)空指針。
- ViewStub中l(wèi)ayout_*屬性都是為新加載的視圖的根視圖設置的,與<include>標簽一樣委煤,ViewStub加載的根視圖自身的layout_*屬性會被ViewStub重寫堂油。比如layout_height,它不能指定ViewStub本身的高度碧绞,因為ViewStub本身的高度和寬度都是0府框,它指定的其實是需要加載的布局的根視圖高度。又由于此讥邻,在布局時要注意基于ViewStub的相對布局在ViewStub未inflate之前迫靖,位置與實際位置是有偏差的。
- 一般xml文件中定義的屬性都可以通過代碼設置兴使,同樣ViewStub也可以通過方法setLayoutResource在代碼中動態(tài)設置應該加載的layout文件系宜,此時一個ViewStub就可以根據(jù)邏輯不同使用不同的視圖。
項目問題
項目中可能大家更習慣使用Visibility的切換而不是ViewStub发魄。如果在布局中你有需要設置可見性的地方盹牧,不妨思考是否需要頻繁切換它的可見狀態(tài),是否需要懶加載励幼,如果用戶見到的可能性不大或者它本身也不經(jīng)常切換自身的可見性汰寓,那么可以考慮使用ViewStub,比如『點擊展開詳情』這種類似的功能苹粟。
五.讓ListView起飛
讓ListView更流暢的最重要的一點是要牢記讓主線程從繁雜的任務任務中解放出來有滑,確保磁盤訪問、網(wǎng)絡請求或者數(shù)據(jù)庫操作是在單獨的線程中的嵌削∶茫可以使用StrictMode去驗證你的app是否遵循這一點。
使用后臺線程
使用后臺線程(也成為工作線程)去處理原本打算放在主線程中的復雜邏輯苛秕,以保證主線程更專注的處理UI繪制工作肌访。通常情況下,使用AsyncTask提供了一個非常簡單的方式用于幫助你在主線程之外處理邏輯艇劫。AsyncTask自動的將所有的execute()請求隊列化场靴,然后依次執(zhí)行,當然這一策略不會影響你自己創(chuàng)建的線程池港准。
在下面的樣例代碼中,AsyncTask用于在后臺加載圖片咧欣,并且在圖片加載完成后提供給主線程使用浅缸,它允許在圖片加載過程中使用進度條做界面展示。
// 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提供了額外的方式以允許你提高在多核手機上的并發(fā)處理能力衩椒,此時你需要用executeOnExecutor()替換之前的execute()方法。(注:AsyncTask在剛開始是以單獨線程去處理所有請求的,從1.6開始被修改成以線程池的方式處理所有的請求毛萌,但是從3.0開始又改成了單獨線程處理所有請求苟弛,想想谷歌是挺好玩的。不過正如這里說3.0版本之后你可以使用executeOnExecutor方法去制定自己的AsyncTask線程池阁将。)
使用View Holder保持視圖對象
你可能要使用findViewById()方法去獲取視圖對象膏秫,但是如果getView時也這么做的話,那么滾動的過程中就會觸發(fā)N多次該方法的調用做盅,這一點即便在Adapter提供了滾動過程中使用復用視圖以避免重復inflate也無法得到改觀缤削。一個比較好的方法去避免N多次調用findViewById是使用『view holder』。
一個ViewHolder對象存儲了ListView中的Item的Layout中所需要的視圖組件吹榴,所以使用了ViewHolder之后你可以直接訪問到它們而無需多次調用findViewById亭敢。為了使用ViewHolder首先你需要定義自己的類
static class ViewHolder {
TextView text;
TextView timestamp;
ImageView icon;
ProgressBar progress;
int position;
}
然后在需要的時候創(chuàng)建并存儲它
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é)約了很多資源图筹。
項目問題
后臺線程還有ViewHolder帅刀,大家使用的已經(jīng)很多了,應該沒有特別大的問題远剩。