Android佈局是應用的重要組成部分,它直接影響到用戶的體驗。如果佈局不合理則會導致內存佔用過多且UI卡頓。Android SDK提供了一些工具可以幫助我們快速定位到影響性能的佈局問題,一般可從以下幾個方面來進行佈局優化。
優化佈局層次結構
衆所周知,複雜的網頁加載速度很慢,Android應用也一樣,複雜的佈局結構也將引起性能問題。下面來說明如何使用工具來檢查佈局並發現性能瓶頸。
我們知道,應用中的每個組件及佈局都需要初始化、測量、繪製等流程,例如使用了嵌套的LinearLayout將會導致更深的View層次,一旦嵌套的LinearLayout使用了layout_weight屬性則將導致更長的加載時間,因爲每個子控件將被測量兩次。這種影響在ListView或GridView中將會更加明顯,因爲這兩個控件的每一個Item都會重用一套佈局。
我們將使用Hierarchy Viewer來檢查並優化佈局。
檢查佈局
Hierarchy Viewer是Android SDK tools提供的一個工具,它位於sdk/tools/目錄下,可以用來分析佈局並發現其中的性能瓶頸。需要注意到是使用Hierarchy Viewer時需要保持App正在運行(使用模擬器或連接真機,真機需要支持調試模式),並保持當前應用的進程正常連接。下面舉一個例子:
上圖是一個ListView的行佈局結構,最外層是一個水平方向的LinearLayout,其內部左側是一個ImageView用來顯示圖片,右側是一個垂直方向的LinearLayout,該LinearLayout又包含上下兩個TextView用來顯示文本。這種佈局結構在我們日常開發中非常常見,接下來我們檢測一下它的佈局性能。
打開Hierarchy Viewer工具後,點擊“Load View Hierarchy”,結果如下圖所示:
從上圖可以看到,這是一個深度爲3層的佈局結構,點擊每一塊佈局則會顯示其測量、佈局、繪製3個過程的時間消耗,如下圖所示:
它說明使用該佈局完全渲染一條列表項的具體耗時爲:
- 測量:0.977ms
- 佈局:0.167ms
- 繪製:2.717ms
這樣開發者就能夠清楚地發現哪裏是佈局的性能瓶頸,接下來就可以針對性地進行優化了。
修改佈局
上述佈局由於使用了嵌套的LinearLayout才導致性能下降,因此提高性能的辦法就是減少佈局的層次嵌套,使之扁平化。我們將最外層的LinearLayout替換成RelativeLayout,使用相對佈局,可以將原來3層的佈局減少爲兩層。再次使用Hierarchy Viewer查看,結果如下:
現在每一行的佈局渲染時間消耗爲:
- 測量:0.598ms
- 佈局:0.110ms
- 繪製:2.146ms
可以看到,性能已經得到了細微的提升,千萬別小看這微小的提升,這個佈局在ListView中可是會被多次調用的,因此整體性能提升是很可觀的。
LinearLayout使用了layout_weight會降低測量的速度,因此在使用權重時務必謹慎,能不用則不用。
使用Lint
我們還可以使用Lint工具來發現佈局中可優化的地方,Lint已經內置到Android Studio中,使用非常方便。Lint有以下常用規則:
- 使用複合Drawable——如果一個LinearLayout包含一個ImageView和一個TextView則使用複合Drawable的方式會更加高效。
- 合併根佈局——如果FrameLayout爲根佈局且沒有背景或內邊距等屬性,則可以使用marge標籤來合併跟佈局以提高效率。
- 無用的葉節點佈局——一個佈局如果沒有子佈局且無背景屬性,則可以移除它,因爲它不會顯示出來,移除之後使得佈局層次更加扁平且高效。
- 無用的父佈局——如果一個佈局(除ScrollView、根佈局之外)沒有背景等屬性,且它的子佈局也無兄弟佈局,就可以將它本身移除,將其子佈局直接移出來。
- 佈局層次過深——佈局層次嵌套過多嚴重影響性能,可考慮使用RelativeLayout或GridLaout來提升性能,建議佈局層次深度不要超過10層。
Lint能夠自動幫助我們修復一些問題、可以提供修改建議或者跳轉到出問題的代碼部分,建議大家好好利用Lint這個工具。
使用include重用佈局
如果某一種特定的佈局結構在應用中出現多次,則可以使用include來重用該佈局,提高效率。
定義重用佈局
加入我們想重用某個佈局,可以單獨爲它創建一個xml佈局,例如,定義一個TitleBar(titlebar.xml),每一個Activity都可以重用它。titlebar.xml內容如下:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/titlebar_bg"
tools:showIn="@layout/activity_main" >
<ImageView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/gafricalogo" />
</FrameLayout>
上述定義中的tools:showIn屬性指定了一個父佈局來include該重用佈局,此屬性將會在編譯時移除,它只是爲了方便在開發時預覽佈局效果。
使用include標籤
在需要重用佈局的地方使用include標籤即可引入佈局,示例如下:
<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標籤下的佈局重寫android:layout_*等屬性,如:
<include android:id="@+id/news_title"
android:layout_width="match_parent"
android:layout_height="match_parent"
layout="@layout/title"/>
使用merge標籤
merge標籤可以幫助我們消除冗餘佈局,例如最外層佈局是一個垂直方向的LinearLayout,它內部是一個可重用的佈局,而這個可重用佈局也是一個垂直方向的LinearLayout包含上下兩個Button。那麼導致的結果是:一個垂直方向的LinearLayout包含另一個垂直方向的LinearLayout,這種冗餘嵌套必然延遲UI的加載效率。
對於上述情況,可以使用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>
按需加載視圖
有時候,一些複雜的View只是偶爾才需要顯示,那麼就可以在需要時再加載它們,這樣減小內存佔用並提高渲染速度,通常的做法就是使用ViewStub。
定義ViewStub
ViewStub是一個輕量級的view,它不會在佈局加載時進行繪製與展示,因此它幾乎沒有性能開銷。每一個ViewStub需要一個android:layout屬性來指定要加載的佈局。
以下ViewStub示例爲一個透明的進度條覆蓋層,它只是在新內容加載時纔會顯示。
<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指定的佈局時,有兩種方法:
- 通過調用
setVisibility(View.VISIBLE)
將其設爲可見 - 調用
inflate()
((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
// or
View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();
需要注意的是inflate()
方法完成後會返回ViewStub指定的佈局,因此不再需要通過findViewById()
來得到此佈局。
ViewStub一旦被inflate或設爲可見,ViewStub元素將不再作爲View層次結構的一部分。原來的ViewStub將會被它指定的佈局所替代,而這個佈局的id就是在ViewStub中指定的android:inflatedId
屬性的值。而原來的ViewStub的id,即Android:id也將無效。
ViewStub的一個缺點是它指定的佈局不支持標籤的使用。
使ListView流暢地滑動
保證ListView流暢滑動的關鍵是應用的主線程沒有耗時的事務處理,一些耗時操作如磁盤訪問、網絡訪問、或數據庫訪問需要使用後臺線程。
使用後臺線程
使用後臺線程可以減輕UI線程的負擔,這樣UI線程可以專注於UI繪製,保證流程的用戶體驗。AsyncTask提供了一種簡單的後臺線程的調用方法,如下示例爲使用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);
從Android 3.0(API level 11)起,可以通過使用executeOnExecutor()
在後臺並行處理多個請求。
使用ViewHolder
在ListView滑動時,會頻繁調用findViewById()
,這將降低性能。即使Adapter返回一個重用的佈局,仍然需要找到對應元素並更新它們。一種可以替代頻繁調用findViewById()
的方式是使用ViewHolder。
ViewHolder對象存儲了每一個view組件,最終通過setTag方法保存到Layout的tag字段中,這樣就能夠快速訪問每個組件。ViewHolder類的創建方法如下:
static class ViewHolder {
TextView text;
TextView timestamp;
ImageView icon;
ProgressBar progress;
int position;
}
填充ViewHolder並把它保存在Layout中:
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);
然後就可以輕鬆地訪問每個視圖,而不需要查找,從而節省寶貴的處理時間。