Android開發學習之路--性能優化之佈局優化

  Android性能優化方面也有很多文章了,這裏就做一個總結,從原理到方法,工具等做一個簡單的瞭解,從而可以慢慢地改變編碼風格,從而提高性能。

一、Android系統是如何處理UI組件的更新操作的

  既然和佈局相關,那麼我們需要了解Android系統是如何處理UI組件的更新操作的。
  1、Android需要把XML佈局文件轉換成GPU能夠識別並繪製的對象。這個操作是在DisplayList的幫助下完成的。DisplayList持有所有將要交給GPU繪製到屏幕上的數據信息。
  2、CPU負責把UI組件計算成Polygons,Texture紋理,然後交給GPU進行柵格化渲染。
  3、GPU進行柵格化渲染。

   那麼什麼是柵格化呢?Resterization柵格化是繪製那些Button,Shape,Path,String,Bitmap等組件最基礎的操作,它把那些組件拆分到不同的像素上進行顯示,如上圖所示,手機的屏幕其實都是有一個一個小格子組成的圖片,而當這些小格子非常非常小的時候,那麼久看起來像1中所示一樣了。這是一個很費時的操作,因此這也是爲什麼要引入GPU原因。
  每次從CPU轉移到GPU是一件很麻煩的事情,所幸的是OpenGL ES可以把那些需要渲染的文理Hold在GPU的Memory裏面,在下次需要渲染的時候直接進行操作。
  Android裏面那些由主題所提供的資源,例如Bitmap,Drawables都是一起打包到統一的Texture文理當中, 然後再傳遞到GPU裏面,這意味着每次你需要使用這些資源的時候,都是直接從文理裏面進行獲取渲染的。當然隨着UI組件的越來越豐富,有了更多演變的形態。例如顯示圖片的時候,需要先經過CPU的計算加載到內存中,然後傳遞到GPU進行渲染。文字的顯示更加複雜,需要先經過CPU換算成紋理,然後再交給GPU進行渲染,回到CPU繪製單個字符的時候,再重新引用經過GPU渲染的內容。動畫則是一個更加複雜的流程。
  爲了能夠使得APP流暢,我們需要在每一幀16ms以內完成所有的CPU與GPU計算,繪製,渲染等等操作。也就是幀率爲60fps,爲什麼幀率要爲60fps呢,因爲人眼與大腦之間的協作無法感知超過60fps的畫面更新。開發app的性能目標就是保持60fps,這意味着每一幀你只有16ms=1000/60的時間來處理所有的任務。這裏需要了解下刷新率和幀率:
  Refresh Rate:代表了屏幕在一秒內刷新屏幕的次數,這取決於硬件的固定參數,例如60HZ。
  Frame Rate:代表了GPU在一秒內揮之操作的幀數,例如30fps,60fps。
  此外這裏引入了VSYNC的機制,Android就是通過VSYNC信號來同步UI繪製和動畫,使得它們可以獲得一個達到60fps的固定的幀率。GPU會獲取圖形數據進行渲染,然後硬件負責把渲染後的內容呈現到屏幕上,他們兩者不停地進行協作。不幸的是,刷新頻率和幀率並不是總能保持相同的節奏。如果發生幀率與刷新頻率不一致的情況,就會容易出現顯示內容發生斷裂,重疊。
  幀率超過刷新率只是理想的狀況,在超過60fps的情況下,GPU所產生的幀數據會因爲等待VSYNC的刷新信息而被Hold住,這樣能夠保持每次刷新都有實際的新的數據可以顯示。

  
  但是我們遇到更多的情況是幀率小於刷新頻率。在這種情況下,某些幀顯示的畫面內容就會與上一幀的畫面相同。糟糕的事情是,幀率從超過60fps突然掉到60fps以下,這樣就會發生LAG,JANK,HITCHING等卡頓停頓的不順滑的情況,這也是用戶感受不好的原因所在。
  在某個View第一次需要被渲染時,DisplayList會因此而被創建,當這個View要顯示到屏幕上時,我們會執行GPU的繪製指令來進行渲染。如果你在後續有執行類似移動這個view的位置等操作而需要再次渲染這個View時,我們就僅僅需要額外操作一次渲染指令就夠了。然而如果修改了View中的某些可見組件,那麼之前的DisplayList就無法繼續使用了,我們需要回頭重新創建一個DisplayList並且重新執行渲染指令並更新到屏幕上。
  需要注意的是:任何時候View中的繪製內容發生變化時,都會重新執行創建DisplayList,渲染DisplayList,更新到屏幕上等一系列操作。這個流程的表現性能取決於View的複雜程度,View的狀態變化以及渲染管道的執行性能。
所以我們需要儘量減少Overdraw。
  Overdraw(過度繪製):描述的是屏幕上的某個像素在同一幀的時間內被繪製了多次。在多層次的UI結構裏面,如果不可見的UI也在做繪製的操作,就會導致某些像素區域被繪製了多次,浪費大量的CPU以及GPU資源。(可以通過開發者選項,打開Show GPU Overdraw的選項,觀察UI上的Overdraw情況)

  藍色、淡綠、淡紅,深紅代表了4種不同程度的Overdraw的情況,我們的目標就是儘量減少紅色Overdraw,看到更多的藍色區域。

二、Android佈局優化常用方法

  綜上,佈局的優化其實說白了就是減少層級,越簡單越好,減少overdraw,就能更好的突出性能。
  下面介紹幾種佈局優化的方式:

1、首先是善用相對佈局Relativelayout

  在RelativeLayout和LinearLayout同時能夠滿足需求時,儘量使用RelativeLayout,這一點可以從我們MainActivity默認佈局就可以看出,默認是RelativeLayout,因爲可以通過扁平的RelativeLayout降低LinearLayout嵌套所產生布局樹的層級。
  Android提供了幾種方便的佈局管理器,大多數時候,你只需要這些佈局的一部分基本特性去實現UI。 一般情況下用LinearLayout的時候總會比RelativeLayout多一個View的層級。而每次往應用裏面增加一個View,或者增加一個佈局管理器的時候,都會增加運行時對系統的消耗,因此這樣就會導致界面初始化、佈局、繪製的過程變慢。還是舉個例子吧,先看一下佈局圖。
  首先用LinearLayout方式來實現:

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

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:src="@mipmap/ic_launcher" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:layout_marginTop="16dp"
            android:text="這個是LinearLayout"
            android:textSize="16sp" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:layout_marginTop="10dp"
            android:text="這個是LinearLayout,這個是LinearLayout"
            android:textSize="12sp" />
    </LinearLayout>

</LinearLayout>

  
  接着是RelativeLayout方式:

<?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/iv_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:src="@mipmap/ic_launcher" />

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="16dp"
        android:layout_toRightOf="@+id/iv_image"
        android:text="這個是LinearLayout"
        android:textSize="16sp" />

    <TextView
        android:id="@+id/tv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/tv_title"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="10dp"
        android:layout_toRightOf="@+id/iv_image"
        android:text="這個是LinearLayout,這個是LinearLayout"
        android:textSize="12sp" />

</RelativeLayout>

  
  很明顯Relativelayout的層級比linearlayout的層級少了一層,這個界面比較簡單,但是如果界面很複雜的情況下,那麼怎麼來優化或者看到一目瞭然的層級樹呢?別擔心,Android自帶了工具,下面就介紹下Hierarchy View的簡單使用吧。
  爲了考慮安全問題,真機上不好嘗試,雖然也有辦法可以解決,但是現在模擬器還是挺快的,也不用折騰那麼多了,就可以直接用模擬器來實現好了,接着模擬器運行app,打開需要獲取view層級的那個界面。
  然後依次點擊菜單Tools -> Android -> Android Device Monitor。

  
  打開Android Device Monitor後,選中Hierarchy View,然後通過Hierarchy View來獲取當前的View的分級圖。

  
  接着我們就可以來看一下兩個分級的不同了。這裏主要截取兩個不同的地方:
  首先是LinearLayout:

  
  接着是RelativeLayout:

  
  很明顯的可以看出來RelativeLayout比LinearLayout少了一個層級,當然渲染的時間也是大大減少了。

2、佈局優化的另外一種手段就是使用抽象佈局標籤include、merge、ViewStub

  2.1、首先是include標籤:
  include標籤常用於將佈局中的公共部分提取出來,比如我們要在activity_main.xml中需要上述LinearLayout的數據,那麼就可以直接include進去了。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.jared.layoutoptimise.MainActivity">

    <include layout="@layout/item_test_linear_layout" />

</RelativeLayout>

  2.2、首先是merge標籤:
  merge標籤是作爲include標籤的一種輔助擴展來使用,它的主要作用是爲了防止在引用佈局文件時產生多餘的佈局嵌套。
  Android渲染需要消耗時間,佈局越複雜,性能就越差。如上述include標籤引入了之前的LinearLayout之後導致了界面多了一個層級。

  這個時候用merge的話,就可以減少一個層級了,代碼如下:

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

    <ImageView
        android:id="@+id/iv_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:src="@mipmap/ic_launcher" />

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="16dp"
        android:layout_toRightOf="@+id/iv_image"
        android:text="這個是MergeLayout"
        android:textSize="16sp" />

    <TextView
        android:id="@+id/tv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/tv_title"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="10dp"
        android:layout_toRightOf="@+id/iv_image"
        android:text="這個是MergeLayout,這個是MergeLayout"
        android:textSize="12sp" />

</merge>

  activity_main就可以直接include了。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.jared.layoutoptimise.MainActivity">

    <include layout="@layout/item_merge_layout" />

</RelativeLayout>

  然後看下層級:

  
  2.3、首先是viewstub標籤:
  viewstub是view的子類。他是一個輕量級View, 隱藏的,沒有尺寸的View。他可以用來在程序運行時簡單的填充佈局文件。接着簡單試用下viewstub吧。首先修改activity_main.xml文件如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.jared.layoutoptimise.MainActivity">

    <include
        android:id="@+id/layout_merge"
        layout="@layout/item_merge_layout" />

    <Button
        android:id="@+id/btn_view_show"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:text="顯示ViewStub"
        android:textAllCaps="false"
        android:layout_below="@+id/tv_content"/>

    <Button
        android:id="@+id/btn_view_hide"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:layout_marginLeft="50dp"
        android:layout_toRightOf="@+id/btn_view_show"
        android:text="隱藏ViewStub"
        android:textAllCaps="false"
        android:layout_below="@+id/tv_content"/>

    <ViewStub
        android:id="@+id/vs_test"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout="@layout/item_test_linear_layout"
        android:layout_below="@+id/btn_view_show"
        android:layout_marginTop="10dp" />

</RelativeLayout>

  這裏的ViewStub控件的layout指定爲item_test_linear_layout。當點擊button隱藏的時候不會顯示item_test_linear_layout,而點擊button顯示的時候就會用item_test_linear_layout替代ViewStub。

3、Android最新的佈局方式ConstaintLayout

  ConstraintLayout允許你在不適用任何嵌套的情況下創建大型而又複雜的佈局。它與RelativeLayout非常相似,所有的view都依賴於兄弟控件和父控件的相對關係。但是,ConstraintLayout比RelativeLayout更加靈活,目前在AndroidStudio中使用也十分方便,就和以前的拖拉控件十分相似。那麼怎麼使用呢?
  首先是安裝Constaintlayout了。Android SDK -> SDK Tools -> Support Repository中的ConstrainLayout for Android和Solver for ConstaintLayout。

  
  然後build.gradle中添加

compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4'

  然後同步下就可以正常使用ConstaintLayout了。

  接着我們來實現上述的佈局文件:
  首先把佈局按照constraintLayout的方式來布。效果如下,其實很像ios的佈局,具體怎麼用的就不一一介紹了:

  
  看下代碼是怎樣的:

<?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:id="@+id/lay_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/iv_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:src="@mipmap/ic_launcher"
        android:layout_marginStart="16dp"
        app:layout_constraintLeft_toLeftOf="parent"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="16dp"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@+id/iv_image"
        android:text="這個是ConstraintLayout"
        android:textSize="16sp"
        app:layout_constraintLeft_toRightOf="@+id/iv_image"
        android:layout_marginStart="20dp"
        android:layout_marginLeft="20dp"
        android:layout_marginTop="20dp"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_content"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_below="@+id/tv_title"
        android:layout_toRightOf="@+id/iv_image"
        android:text="這個是ConstraintLayout,這個是RelativeLayout"
        android:textSize="12sp"
        app:layout_constraintTop_toBottomOf="@+id/tv_title"
        android:layout_marginTop="16dp"
        app:layout_constraintLeft_toLeftOf="@+id/tv_title" />

</android.support.constraint.ConstraintLayout>

  最後運行之後看下佈局的層級如下:

  
  這個是簡單的佈局,如果是複雜的佈局的話,那用Constaintlayout的話最多就兩個層級了,不像Relative和Linear一樣一層嵌套一層的。

4、利用Android Lint工具尋求可能優化佈局的層次

  一些Lint規則如下:
  1、使用組合控件: 包含了一個ImageView以及一個TextView控件的LinearLayout如果能夠作爲一個組合控件將會被更有效的處理。
  2、合併作爲根節點的幀佈局(Framelayout) :如果一個幀佈局時佈局文件中的根節點,而且它沒有背景圖片或者padding等,更有效的方式是使用merge標籤替換該Framelayout標籤 。
  3、無用的葉子節點:通常來說如果一個佈局控件沒有子視圖或者背景圖片,那麼該佈局控件時可以被移除(由於它處於 invisible狀態)。
   4、無用的父節點 :如果一個父視圖即有子視圖,但沒有兄弟視圖節點,該視圖不是ScrollView控件或者根節點,並且它沒有背景圖片,也是可以被移除的,移除之後,該父視圖的所有子視圖都直接遷移至之前父視圖的佈局層次。同樣能夠使解析佈局以及佈局層次更有效。
  5、過深的佈局層次:內嵌過多的佈局總是低效率地。考慮使用一些扁平的佈局控件,例如 RelativeLayout、GridLayout ,來改善佈局過程。默認最大的佈局深度爲10 。

github代碼

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