Android佈局性能優化指南

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);

然後就可以輕鬆地訪問每個視圖,而不需要查找,從而節省寶貴的處理時間。

參考文獻:Improving Layout Performance

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章