Android 性能優化之布局優化

在 Android 開發的過程中,對性能進行優化是必不可少的一步。而在布局上的優化并不像其他優化方式那么復雜,通過 Android SDK 提供的 Hierarchy Viewer 可以很直接地看到冗余的層級,去除這些多余的層級將使我們的UI變得更加流暢。

如何使用 Hierarchy Viewer ?
  1. 在 Android studio 菜單欄上,點擊 Tools --> Android --> Android Device Monitor

  2. 在 Android Device Monitor 菜單欄上,點擊 Window --> Open perspective --> Hierarchy Viewer 即可。

然而,一般在真機上無法使用 Hierarchy Viewer,只能在運行開發版 Android 系統的設備進行交互(一般來說,使用模擬器上即可),著實有點遺憾。但是,也可以在真機上通過 root 等一系列操作之后,便可以使用 Hierarchy Viewer 。這部分教程就需要大家在網上另行查閱了。

下面是一些常用的布局優化方式:

一、include 布局

當多個頁面公用了一些UI組件時,就可以使用 include 布局。Android 提供了 include 標簽,讓我們可以將子布局引入到一個布局文件中,這樣一來,公用布局就可以獨立成為一個布局 xml,其他頁面只要 include 引用這個布局xml即可。

下面以一個自定義標題欄為例

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ImageButton
        android:id="@+id/title_back_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_centerVertical="true"
        android:src="@drawable/back" />

    <TextView
        android:id="@+id/title_textview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Title" />
</RelativeLayout>

將該標題欄引入一個Activity的xml中

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.jerry.myapplication.MainActivity">

    <include
        android:id="@+id/top_title"
        layout="@layout/common_title" />

    <TextView
        android:id="@+id/username_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

通過 include 標簽引入 common_title 這個布局(注意:include 布局中指定布局 xml 是使用 layout 屬性,而不是 android:layout 屬性)。這樣一來,就復用了 common_title 的標題欄效果,不必在每個頁面中重復定義標題欄布局。大大降低了我們維護 xml 的成本,也提升了代碼復用率。

include 標簽的原理:

在解析 xml 布局時,如果檢測到 include 標簽,那么就直接把該布局下的根視圖添加到 include 所在的父視圖中。對于布局 xml 的解析最終都會調用到 LayoutInflaterinflate 方法,該方法最終又會調用 rInflate 方法。這個方法就是遍歷 xml 中的所有元素,然后逐個進行解析。

如何獲取 include 布局里的控件?

以上面例子為例,獲取標題欄內的 Textview

 private TextView mTextView;

 mTextView = findViewById(R.id.top_title).findViewById(R.id.title_textview);

二、merge 標簽

merge 標簽適用于子布局的根視圖與它的父視圖是同一類型。它的作用是合并UI布局,降低UI布局的嵌套層次。使用場景是存在多層使用同一種布局類的嵌套視圖,這種情況下用merge標簽作為子視圖的頂級視圖來解決多余的層級。


如上圖,child_view 與 parent_view 都是FrameLayout類型,那么 child_view 下的兩個控件可以直接使用 parent_view 來布局,這樣就可以去除 child_view 這個層級。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</FrameLayout>

上面這個布局相當于是 child_view,而 Activity 內容視圖的頂層布局也 FrameLayout,因此產生了視圖冗余,可以用 merge 標簽去掉這層冗余。

下面是screen_title.xml文件的源代碼:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:fitsSystemWindows="true">
    <!-- Popout bar for action modes -->
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
        android:layout_width="match_parent" 
        android:layout_height="?android:attr/windowTitleSize"
        style="?android:attr/windowTitleBackgroundStyle">
        <TextView android:id="@android:id/title" 
            style="?android:attr/windowTitleStyle"
            android:background="@null"
            android:fadingEdge="horizontal"
            android:gravity="center_vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
    <!-以下就是開發人員設置的布局所填充的位置-->
    <FrameLayout android:id="@android:id/content"
        android:layout_width="match_parent" 
        android:layout_height="0dip"
        android:layout_weight="1"
        android:foregroundGravity="fill_horizontal|top"
        android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
<?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" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</merge>

與 include 一樣,merge 的解析也在 LayoutInflater 的 inflate() 函數中。在 inflate() 函數中循環解析 xml 中 的 tag,如果解析到 merge 標簽則會調用 rinflate 函數。

注意事項:
  1. merge 必須作為布局文件的根節點標簽。

  2. merge 并不是一個 ViewGroup,也不是一個 View,它相當于聲明了一些視圖,等待被添加。

  3. 因為 merge 標簽不是 View,所以對 merge 標簽設置的所有屬性都是無效的。

  4. 因為 merge 標簽不是 View,所以在通過 LayoutInflate.inflate 方法渲染的時候,第二個參數必須指定一個父容器,且第三個參數必須為 true,也就是必須為 merge 下的視圖指定一個父親節點。

  5. 如果 Activity 的布局文件根節點是 FrameLayout,可以替換為 merge 標簽,這樣,執行 setContentView 之后,會減少一層 FrameLayout 節點。

三、ViewStub 視圖

ViewStub 是什么?

ViewStub 是一個不可見的和能在運行期間延遲加載目標視圖的、寬高都為0的 View (ViewStub 繼承 View)。當對一個 ViewStub 調用 inflate() 方法或設置它可見時,系統會加載在 ViewStub 標簽中指定的布局,然后將這個布局的根視圖添加到 ViewStub 的父視圖中。換句話說,在對 ViewStub 調用 inflate() 方法或者設置 visible 之前,它是不占用布局空間和系統資源的,它只是為目標視圖占了一個位置而已。

何時使用 ViewStub?

當我們只需要在某些情況下才加載一些耗資源的布局時,ViewStub 就成為我們實現這個功能的重要手段。

例如:有一個顯示九宮格圖片的 GridView 視圖,我們想根據網絡返回的數據來判斷是否加載該 GridView ,因為默認加載的話會造成資源浪費,系統加載成本較高。這時使用 ViewStub 標簽就可以很方便實現延遲加載。

以下是一個小小的演示,在一個 Activity 中使用 ViewStub 來動態加載
GridView。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <include
        android:id="@+id/top_title"
        layout="@layout/common_title" />

    <Button
        android:id="@+id/show_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="顯示圖片" />

    <ViewStub
        android:id="@+id/comment_stub"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout="@layout/layout_image_gv" />

</RelativeLayout>

布局的最后使用 ViewStub 來加載 layout_image_gv.xml 布局,下面是 layout_image_gv.xml 的代碼:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#aaaaaa">

    <TextView
        android:id="@+id/image_desc"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="顯示九宮格GridView" />

    <GridView
        android:id="@+id/image_gc"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/image_desc"
        android:numColumns="3">

    </GridView>
</RelativeLayout>
public class StubLayoutActivity extends AppCompatActivity {

    private ViewStub gvStub;
    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_activity_stub);

        gvStub = findViewById(R.id.comment_stub);

        mButton = findViewById(R.id.show_btn);

        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //加載目標視圖,不能多次調用,否則會引發異常
                gvStub.inflate();
                //gvStub.setVisibility(View.VISIBLE);

            }
        });
    }
}
ViewStub 小結

當用戶手動調用 ViewStub 的 inflate 或者 setVisibility 函數(實際上也是調用 inflate 函數)時,會將 ViewStub 自身從父控件中移除,并且加載目標布局,然后將目標布局添加到 ViewStub 的父控件中,這樣就完成視圖的動態替換,也就是延遲加載功能。

四、減少視圖樹層級

為什么要減少視圖層級?

每一個視圖在顯示時會經歷測量、布局、繪制的過程,如果我們的布局中嵌套的視圖層次過多,就會造成額外測量、布局等工作,使得UI變得卡頓,影響用戶的使用體驗。

例如一個簡單的列表 Item

<?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">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

而使用 RelativeLayout 來布局這個 item view 就可以減少一層 LinearLayout 的渲染。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/profile_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/name_textview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/profile_image" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/profile_image" />
</RelativeLayout>

五、總結

在 Android UI 布局過程中,需要遵守的原則有以下幾點:

  1. 盡量多使用 RelativeLayout ,不要使用絕對布局 AbsoluteLayout;

  2. 在 ListView 等列表組件中盡量避免使用 LinearLayout 的 layout_weight 屬性(子視圖使用了 layout_weight 屬性的 LinearLayout 會對它的子視圖進行兩次測量。)

  3. 將可復用的組件抽取出來并通過 <include/> 標簽使用;

  4. 使用 <ViewStub/> 標簽來加載一些不常用的布局;

  5. 使用 <merge/> 標簽來減少布局的嵌套層次。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,460評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,067評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,467評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,468評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,184評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,582評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,616評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,794評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,343評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,096評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,291評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,863評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,513評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,941評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,190評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,026評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,253評論 2 375

推薦閱讀更多精彩內容