Android UI 優化指南

太長不看版:

在 Android UI 布局過程中,遵守一些慣用、有效的布局原則,可以制作出高效且復用性高的 UI。

  1. 盡量多使用 ConstraintLayout、RelativeLayout、LinearLayout
  • 盡量使用 ConstraintLayout
  • 在布局層級相同的情況下,使用 LinearLayout 代替 RelativeLayout
  • 在布局復雜或層級過深時,使用 RelativeLayout 代替 LinearLayout 使界面層級扁平化
  1. 將可復用的組件抽取出來并通過 include 標簽使用
  2. 使用 merge 減少布局的嵌套層級
  3. 使用 ViewStub 加載一些不常用的布局
  4. 盡可能少用 layout_weight
  5. 去除不必要的背景,減少過度繪制
  • 有多層背景重疊的,保留最上層。或者可以統一的使用一個大的背景
  • 對于 Selector 當背景的,可以將 normal 狀態的 color 設置為 @android:color/transparent

完整閱讀本文需要 15 分鐘

減少過度繪制

過度繪制(Overdraw)是指應用在渲染一幀的時間內對屏幕某個像素進行多次繪制。應用應該盡可能避免過度繪制,因為讓 GPU 去繪制用戶不可見的內容完全是一種浪費。

為什么會過度繪制

例如,在多層次的重疊的 UI 結構中,上層的 UI 遮擋了下層的 UI,系統是從后往前進行繪制(painter's algorithm),被遮擋的部分仍然會被繪制。為什么系統不直接繪制需要表現的 UI 而是從后往前繪制呢?系統采用此繪制算法是為了能給半透明的對象如陰影添加合適的透明度。

一般情況下,UI 元素由 XML 布局及自定義控件中定義。因此導致過度繪制的主要原因為:

  • XML 布局 → 控件重疊;多次設置了背景
  • 自定義 View → onDraw() 方法中同一個區域被多次繪制

診斷過度繪制

初步查看

在設置中的開發者選項提供了過度繪制檢測工具,此工具能夠展示頁面上哪些區域出現了不必要的過度繪制,可以直觀查看應用當前頁面是否存在過度繪制的現象,并且可以直觀對比優化前后的顯示效果。

按照以下步驟開啟:

  1. 點擊設置中的開發者選項
  2. 點擊調試 GPU 過度繪制
  3. 彈出框中選擇顯示過度繪制區域

這時可以看到頁面出現了不同顏色的色塊。不同的顏色代表不同程度的過度繪制:

過度繪制區域顏色輸出
顏色 Overdraw 倍數 像素點繪制次數 可接受區域大小
無色 0 倍 1 全部
藍色 1 倍 2 大片
綠色 2 倍 3 中等
粉色 3 倍 4 小于 1/4
紅色 4 倍 5 避免紅色

過度繪制的優化目標是使得顯示區域的過度繪制色塊大部分為無色或者為藍色,當然這個是比較理想的效果,不大面積出現紅色即可視為達到目標了。

以天氣應用為例,開啟顯示過度繪制區域后顯示如下:

天氣應用過度繪制

可以看到列表區域存在紅色程度的過度繪制的情況。

詳細定位

通過初步查看,看到了存在過度繪制的區域,但是這時候還不知道過度繪制的區域是由哪幾層重疊形成的,這時候我們可以借助工具進一步定位。

最簡便的方法便是借助常用的 Hierarchy View 工具進行分析。

  1. 打開 Android Studio 的 Tools → Android → Android Device Monitor
  2. 打開 Hierarchy View
Hierarchy View
  1. Windows 窗口中選擇相應的 Activity
Activity Tree View
  1. 使用圖片中圓圈標注的功能:Capture the window layers as a Photoshop document 將當前界面導出為一個帶圖層信息的 Photoshop 的文件
導出.psd文件
  1. 使用 GIMP 軟件(Ubuntu)打開該 .psd 文件查看圖層信息
psd.jpg

可以看到城市列表的背景部分被設置了兩次。查看代碼,發現是給列表每一項的layout都設置了背景,但是由于該列表的內容都會填充滿該項的位置,故可以去掉該背景。

修復過度繪制

通過工具進行定位,找到了需要優化的頁面,如上原因所述,過度繪制的成因分為兩個方面,因此修復過度繪制分別從兩方面著手。

XML 布局優化

去除不必要的背景

首先要做的就是去除不必要的背景,多個有背景的布局控件放在一起就有可能導致過度繪制。

被上層視圖背景覆蓋下的內容可能永遠都不會被用戶看到,當子視圖具有背景覆蓋了父視圖,特別是它們如果使用了相同的背景色時你將很不容易發現,這就需要上面的檢測工具來定位過度繪制區域是由哪些層級的元素所覆蓋形成的。一般的,優化布局移除背景可以總結為以下幾點:

  • 移除 XML 中不必要的背景,或根據條件設置
  • 移除 Window 默認的背景
  • 按需顯示占位圖片

其中,第二點指的是使用Android的自帶的主題時,往往設置了一個默認的背景,這個背景由DecorView持有。當 App 的布局擁有另外的全局背景的時候,這個主題帶的背景就是多余的,因此可以移除:

  • 可以在Style里添加:
<item name="android:windowBackground">@null</item>
  • 或者在 Activity 的 onCreate 方法中添加:
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getwindow().setBackgroundDrawable(null); 
}

舉個去除背景的例子,便簽應用優化前,存在一層不必要的過度繪制。

未去除背景前

通過以上導出圖層的方法進行查看后發現子視圖 id/sv_note_editor 和 id/statusBarBackground 重復設置了背景。此處可以去除子 View 的背景,去除之后查看過度繪制情況,可以發現應用減少了一層過度繪制。

去除背景后
減少透明元素

在屏幕上顯示透明的像素,稱為 α 渲染,該情況也是導致過度繪制的一個關鍵因素。與標準的過度繪制不同,標準的過度繪制中是在上面繪制不透明的像素來完全覆蓋已繪制的像素,而一個透明的對象必須要多層次繪制才能實現透明效果。如下圖所示,每個有懸浮按鈕控件的頁面上可以看到一坨過度繪制。

懸浮按鈕:帶陰影

事實上,透明動畫,漸隱效果和陰影效果等視覺效果都涉及到了某種程度的透明,而這很有可能會導致過度繪制,因此通過減少渲染透明對象的數量可以減少過度繪制。例如,你可能會通過給一個字體顏色為黑色的 TextView 設置一個透明度來得到一個灰色的文本,可以替換成直接繪制灰色的文本,這樣能得到同樣的效果,但是性能更好。

減少視圖的層級

現在的設計的布局在每個視圖對象都不透明的情況下,可能在屏幕上已經發生了重疊,如果是因為這種情況產生了過度繪制,可以通過優化視圖層次結構來提高性能,以減少重疊的 UI 對象的數量。具體的減少視圖層級的分析后文會繼續提到。

自定義 View 優化

在自定義 View 中的 onDraw 方法通過兩個常用的方法來避免卡片式重疊(矩形式重疊)導致的過度繪制。關于以下這兩個方法的使用,在你需要使用的時候上網查一下例子就知道了。

快速判斷是否需要繪制

在繪制一個區域之前,首先通過 canvas.quickReject() 方法判斷該區域是否不和 Canvas 的剪切域(指定繪制區域)相交。返回 true 表示該區域與指定繪制區域不相交,這時直接繪制無須 GPU 的計算與渲染即不產生過度繪制;返回 false 即表示該區域與指定繪制區域相交了,這時可以指定自己的繪制區域為與原先剪切域 diff 的區域:canvas.clipRect(rectF, Region.Op.DIFFERENCE)

指定繪制區域

每個繪制單元都有自己的繪制區域,繪制前,canvas.clipRect(Region.Op.INTERSECT) 幫助系統識別那些可見的區域。這個方法可以指定一塊矩形區域,只有在這個區域內才會被繪制,其他的區域會被忽視。這個API 可以很好的幫助那些有多組重疊組件的自定義View來控制顯示的區域。同時 clipRect 方法還可以幫助節約 CPU 與 GPU 資源,在clipRect區域之外的繪制指令都不會被執行,那些部分內容在矩形區域內的組件,仍然會得到繪制。

通過以上幾項措施,可以有效修復過度繪制。當然,正如前文所述,完全消除過度繪制是一種理想化,業務中不可避免地需要對控件進行復用、封裝,很容易在不知不覺中產生過度繪制,對此情況只要保持警惕,時刻檢查,選擇合適的手段,就能最大可能地避免嚴重的過度繪制現象。例如在 ListView 中每個 ItemView 如果相當復雜,就可以實現成一個自定義 View,通過指定繪制區域和重寫 requestLayoutonSizeChanged 等措施來優化,可以使得滑動更加流暢。

視圖層級

繪制過程包括一個測量(Measure)和布局(Layout)的過程。測量部分決定了 View 的大小:尺寸和邊界;布局部分決定了 View 在屏幕上的位置。

大多數時候,每個 View 在這兩個過程的消耗都很少,不會影響到性能。然而,當應用添加或者移除一些 View 對象的時候,例如當一個 Recyclerview 重用條目的時候,這個消耗會變大。另外當一個 View 對象是自適應時消耗也會更高,例如,一個 wrapcontent 的 TextView 對象調用了 setText() 方法時候,它需求重新計算尺寸。如果上述情況消耗時間過長,就會導致一幀無法在規定的 16ms 中完成繪制,那么這些幀就會被丟棄,用戶就可能覺得卡頓。

但是因為 UI 操作只能在主線程中執行,你不能將它們移到子線程中去執行,所以最好還是對視圖進行優化,減少它們的時間消耗。

以下先介紹兩種會影響繪制性能的問題。

繪制性能

復雜度:不同布局與深度

在 Android 系統中繪制源碼是在 ViewRootImp 類的 performTraversals() 方法中 ,可以看到 Measure 和 Layout 都是通過以深度優先的遞歸來完成的,需要遍歷子層級的 View,因此,層級越深,元素越多,耗時也就越長,特別在層級太深時,每增加一層會增加更多的耗時。

常見的繪制耗時長的布局的特點就是視圖層級深,進行了多層的嵌套。每一層嵌套都給布局增加了消耗,因此解決問題的根本辦法就是使視圖層級變得扁平。舉個例子,使用 RelativeLayout 進行的布局與嵌套的無權重的 LinearLayout 效果相同,由于 RelativeLayout 有下文提到的 Double Taxation 現象,所以 LinearLayout 布局的性能更好,但如果此時深度很深,就要考慮增加層級是否是正確的。

Double Taxation

通常情況下,布局或者測量的過程只需要進行一次,這個過程一般都很快。然而,在一些復雜的布局情況下,可能需要多次遍歷層次結構的各個部分,這些部分需要經過多次測量才能最終定位。這種需要執行超過一次的布局和測量的迭代叫做Double Taxation。以下是不同的布局的 Double Taxation 現象:

RelativeLayout

當使用 RelativeLayout 時候,它需要根據一個 View 的位置來確定另一個 View 的位置:

  1. 執行第一次布局-測量的過程,在這個過程中,根據每個子 View 的需求計算它們的位置和尺寸
  2. 通過這些數據,再結合這些 View 對象的權重,確定它們的合適的位置
  3. 執行第二次的布局過程來最終確定這些視圖的位置

也就是說 RelativeLayout 布局一定會做兩次測量。

LinearLayout

LinearLayout 如果為橫向布局時候,需要執行兩次布局-測量過程。在豎向布局時候,如果添加了 measureWithLargestChild 屬性,也有可能會需要執行兩次的布局-繪制過程,因為在這種情況下,Framework 可能需要執行兩次流程來確定對象的合適尺寸。

GridLayout

GridLayout 也允許相對放置 View,它通常是通過預處理子 View 之間的位置關系來避免雙倍消耗。然而,當它使用 Weight 或者 Gravity 屬性時,就會失去預處理的優勢,如果再包含有 RelativeLayout 的話,此時可能就要更多次的布局測量流程。

事實上,多次的布局-測量流程即 Double Taxation 本身并不一定是負擔。但是 Double Taxation 發生在以下布局層級中就要注意了:

  • 布局的根元素
  • 有一個深層級的結構
  • 有很多實例填充在屏幕上,類似 ListView 中的子條目

在以上情況中就要盡可能地避免出現 Double Taxation 現象。

通過以上兩點,可以看出選擇 RelativeLayout 還是 LinearLayout 并不是絕對的,本身層級太深的話就推薦使用 RelativeLayout 減少布局本身的層次,否則使用性能更好的 LinearLayout 更合適。

ConstraintLayout

當然,如果應用面向 7.0 開發,可以使用 ConstraintLayout 代替 RelativeLayout,可以避免本節描述的許多問題。ConstraintLayout 提供了與 RelativeLayout 相似的布局控制功能,但是性能更好。因為它與普通的布局不同,就是它使用自己的 constraint-solving 系統來解決視圖之間的關系。

診斷繪制性能

初步查看

Profile GPU rendering

與過度繪制的診斷類似,在設置中的開發者選項提供了 GPU 呈現模式分析(Profile GPU rendering)工具,此工具能夠展示繪制一幀時,布局-測量流程花費了多少時間。

按照以下步驟開啟:

  1. 點擊設置中的開發者選項
  2. 點擊GPU 呈現模式分析
  3. 彈出框中選擇在屏幕上顯示為條形圖

這時可以看到頁面出現了條形圖。每一條條形圖代表每一幀的繪制情況,條形圖上的不同的顏色代表繪制的不同過程:

Profile GPU rendering

可以看到屏幕上有一條綠線,條形圖在綠線之下代表該幀的繪制時間在 16ms 之內,如果一個應用的大部分條形圖都超過了綠線,那么該應用給用戶的感受就是明顯的卡頓感。

條形圖的顏色在不同的系統版本上是不一樣的,在 6.0 及更高的版本有 8 種顏色,在 4.0 (API level 14) 到 5.0 (API level 21) 之間只有 4 種顏色。用表格來描述一下

6.0以上條形圖
4.0 and 5.0 條形圖.jpg

具體的顏色的含義這里就不詳細說了,只需要關注是否超過了綠線以及是否是繪制的測量、布局流程耗時占比很大。GPU Profile 工具可以很簡便地幫助你找到渲染有問題的頁面。

Systrace

Systrace 是 Android 4.1 及以上版本提供的性能數據采樣和分析工具。它可以幫助開發者收集 Android 關鍵子系統(如:surfaceflinger、WindowManagerService 等 Framework 部分關鍵模塊、服務, View 系統)的運行信息,從而幫助開發者更直觀地分析系統瓶頸,改進性能。

Systrace 的功能包括跟蹤系統的 I/O 操作、內核工作隊列、 CPU 負載等,很好收集分析 UI 顯示性能的數據。 Systrace 工具可以跟蹤、收集、檢查定時信息,可以很直觀地查看 CPU 周期消耗的具體時間,顯示每個線程和進程的跟蹤信息,使用了不同的顏色來突出問題的嚴重性,并提供了解決這些問題的一些建議。

使用方法:

  1. 收集 trace 數據(具體可查看:Systrace Walkthrough
    Steps for starting Systrace

    Steps for creating a trace.png

收集 trace 數據還可以通過命令行的方式,使用命令行配置好后多次使用可以快速得到數據,不用每次手動點擊去收集。

$ cd android-sdk/platform-tools/systrace
$ python systrace.py --time=10 -o mynewtrace.html sched gfx view wm

關于命令行的參數及配置請查看:Systrace command reference

  1. 使用 Chrome 打開 trace.html 文件,使用 WASD 進行縮放、移動查看


    Clicking the Alert button to the right reveals the alert tab.png

與 UI 性能相關主要是右上角的 Alerts 選項以及對應的 Frame 數據,Alerts 選項中將列出渲染時間超時的幀,選中該 Alert 可以看到窗口下方展示了該 Frame 問題的詳細數據描述以及相關的建議,并且會定位出對應的 Frame 行的對應位置。

Frame 行上有圓圈,如果是綠色的,表示該幀渲染滿足性能要求,即在 16ms 內渲染完畢,如果是黃色、紅色則代表渲染時間超過了 16ms。使用 W 鍵放大后可以看到系統在這一幀中具體做了什么。具體的相關的信息分析可從網上查找經驗總結博客。

注意:由于 Systrace 是以系統的角度返回一些信息,并不能定位到具體的耗時的方法,要進一步獲取 CPU 被占用的原因,就需要使用另一個分析工具 Traceview。

剛才說到 Systrace 收集展示的是系統的信息,實際上在 4.3 之后,可以通過插入代碼的方式,在 Systrace 里顯示想要查看的 API 的耗時以及調用關系。舉個例子:

public class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {

    ...

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        Trace.beginSection("MyAdapter.onCreateViewHolder");
        MyViewHolder myViewHolder;
        try {
            myViewHolder = MyViewHolder.newInstance(parent);
        } finally {
            Trace.endSection();
        }
        return myViewHolder;
    }

   @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        Trace.beginSection("MyAdapter.onBindViewHolder");
        try {
            try {
                Trace.beginSection("MyAdapter.queryDatabase");
                RowItem rowItem = queryDatabase(position);
                mDataset.add(rowItem);
            } finally {
                Trace.endSection();
            }
            holder.bind(mDataset.get(position));
        } finally {
            Trace.endSection();
        }
    }

…

}

通過 Trace.beginSection 和 Trace.endSection 來追蹤應用的代碼片段,有兩個需要注意的地方:

  1. 這兩個 API 需要放在同一個線程里
  2. 這兩個 API 需要成對出現,而且每一個 endSection 都只會與最近的 beginSection 對應

更多關于 Systrace 的信息請查看 Analyzing UI Performance with Systrace

這樣子通過查看 Systrace 就可以查看到應用的頁面是否存在渲染問題,并且可以初步定位到問題的原因所在,然后可以通過插入代碼增加 trace 的方式去分析例如 ListView 中的 getView 方法的耗時,相對于打 Log 的方式會更加地直觀方便查看耗時數據。

詳細定位

Hierarchy View

上文提到Android Studio 的Hierarchy Viewer有著強大的視圖調試功能 ,它使用圖形化來表現視圖的結構。它呈現的視圖可以用來分析由Double Taxation引起的性能問題。它也可以很容易定位到因為深層嵌套或者嵌套了大量子類的布局導致布局-測量流程非常耗時引起的性能問題。

這里介紹它的另一個功能:Profile Node。


profile node

選擇上圖中紅框圈中的最后一個圖標:obtain layout times for tree rooted as selected node,可以獲得布局-測量流程所消耗的相對時間信息。如下圖所示,需要注意的是,圖中的圓圈的顏色是與同級的視圖相比較得出的,與 Systrace 中顏色的含義有所不同。


profile node result
Lint

Lint 掃描通過靜態掃描檢查代碼的方式,能夠發現在代碼中潛在的問題,同時給出問題的原因和在代碼中的位置,并給出相應的優化建議。

Lint 的功能非常強大,開發者應該深入學習使用方法,可進行配置檢查選項甚至自定義檢查規則。掃描規則和缺陷級別的配置在 File → Settings → Inspections → Android Lint。這里我們只關注 Performance 規則。

Lint Performance

共有 29 項規則,默認選中 27 項,舉些例子:

  1. Layout has too many views:表示控件太多,默認超過 80 個控件會提示該問題
  2. Layout hierarchy is too deep:表示層級太深,默認超過 10 層會提示該問題
  3. Useless parent layout:表示無用的父布局,應該移除避免加深布局層級
  4. Node can be replaced by a TextView with compound drawables:表示可優化的布局,即一個 ImageView 和一個 TextView 線程布局可以使用 CompoundDrawable 的 TextView 代替

一般通過 Lint 掃描都會掃描出代碼中存在性能問題,但是對于具體的問題是否要解決是要衡量一下的,不是說每一個提示都需要去解決。

優化布局層級

布局復用

可重用布局這項功能特別強大,它可以使你創建那些復雜的可重用布局,一個相同的布局可以在很多頁面使用。比方說,可以用來創建一個含有 yes 和 no 按鈕的容器或者一個含有 progressBar 及一個文本框的容器。雖然說你可以通過自定義 View 的方式來實現更為復雜的 UI 組件,但是重用布局的方法更簡便一些,修改起來不會有遺漏。Android 的布局復用通過 include 標簽來實現。

  1. 創建一個可重用的布局
    如果你已經知道哪一個布局需要重用,那么就創建一個新的 xml 文件用來定義這個布局。下面就定義了一個 ActionBar 的布局文件,眾所周知,ActionBar 是會在每個 Activity 中統一出現的:
<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>
  1. 使用 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>

你也可以重寫布局的參數,但只僅限于以 android:layout_* 開頭的布局參數。就像下面這樣:

<include android:id=”@+id/news_title”
         android:layout_width=”match_parent”
         android:layout_height=”match_parent”
         layout=”@layout/title”/>

如果你要重寫 include 標簽指定布局的布局屬性,那么必須重寫 android:layout_height 及 android:layout_width 這兩個屬性,以便使其它屬性的作用生效。

減少層級

使用 Hierarchy View 查找 UI 布局不合理的地方主要關注兩個問題:

  1. 冗余的父布局。
    在視圖樹上可以定位此問題,也就是看到一長串的沒有分支的布局就要注意了,是不是有的布局:沒有背景繪制、沒有大小限制,這種布局就是無用的父布局,如果是在布局文件里不小心寫出了這樣的父布局,使用 Lint 工具可以直接提示你。一般來說引入此問題的原因是使用了 include 標簽,使用此標簽很容易導致不注意的情況下多了一個無用的父布局。對于該問題,可以通過 merge 標簽合并來減少UI層級。
  2. LinearLayout 帶來過深的層級
    同樣的,看到一長串的沒有分支的布局時可以看一下是不是使用 LinearLayout 嵌套布局了,如果有此類問題,可以使用 RelativeLayout 來代替 LinearLayout。但正如上面所說,它們的相互替代的效果是需要衡量的,可以使用 profile node 來檢測前后的效果。
RelativeLayout、LinearLayout、ConstraintLayout

正如上面所說,使用不同的布局可以達到相同的效果,但最終由于層級與測量流程的不同,哪種布局下的性能最好是需要修改后再測量考慮的。但有幾個原則是可以遵循的。

  1. 能使用 ConstraintLayout 就使用它,不能的話盡量使用 RelativeLayout 和 LinearLayout
  2. 在布局層級相同的情況下,使用 LinearLayout
  3. 在層級過深時,使用 RelativeLayout 使界面扁平化
合理使用 merge

在將一個布局內嵌進另一個布局時,merge 標簽可以幫助消除冗余的 View 容器。舉個例子,如果你的主布局是一個垂直的 LinearLayout,在它的內部含有兩個 View,并且這兩個 View 需要在多個布局中重用,那么重用這兩個 View 的布局需要有一個 root View。然而,使用單獨的 LinearLayout 作為這個 root View 會導致在一個垂直的 LinearLayout 中又嵌了一個垂直的 LinearLayout。其實這個內嵌的 LinearLayout 并不是我們真正想要的,此外它還會降低UI性能。

為了避免出現這種冗雜的 View 容器,你可以使用 merge 標簽作為這兩個 View 的 root View:

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

那么現在再使用這個布局的時候,系統會自動忽略 merge 標簽,并會將兩個 Button View 直接加入到布局 < include/> 標簽所指定的位置。

注意:如果 Merge 代替的布局元素為 LinearLayout,在自定義布局代碼中要將 LinearLayout 的屬性添加到引用上,如垂直、水平布局、背景色等。Merge 不是哪里都可以用的,顯然它只能在 xml 文件的根元素上,而且還要注意以下兩點:

  1. 使用 Merge 來加載一個布局時,必須指定一個 ViewGroup 作為其父元素,并且要設置加載的 attachToRoot 參數為 true(參照 inflate(int, ViewGroup, Boolean));
  2. 不能在 ViewStub 中使用 Merge 標簽,原因就是 ViewStub 的 inflate 方法中根本沒有 attachToRoot 的設置。

這里講了如何減少層級,那么多少層才是合理的呢?從 Lint 的檢查配置上來看,超過 10 層才會報警,所以我們可以認為超過 15 層就必須重視開始準備優化,再多層就是一定要修改的了。

提高繪制速度

上文提到繪制需要進行布局與測量,并且層級越深,元素越多,耗時也就越長。在實踐中,有時候我們的布局文件中存在很多只在特定情況下才會使用到的 View。針對這些 View,很多時候我們會使用設置可見性的方式來確保只在需要時才會顯示。

但是設置為 Gone 并不能解決性能問題,繪制流程中還是會測試和解析這些布局的。在 inflate 布局文件的時候,依然會去創建 GONE 屬性的實例,初始化對象。我們知道創建對象以及測量-布局流程耗費很高,如果創建大量當前不需要顯示的 View 對象,會很大程度上增加啟動時間。對于減少內存使用來說,設置可見性也沒有任何用處。

在上述情況下,推遲資源加載是非常重要的解決手段,通過“在需要時才去加載”的方式來降低內存使用和加快繪制速度。Android 提供了 ViewStub 控件來解決這個場景,我們可以通過聲明 ViewStub 來實現推遲 View 加載。ViewStub 是一個輕量級的 View,它的構造函數簡單、成員變量很少,對象創建時間更短。而且它的尺寸為0,并且不會繪制任何東西。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(0, 0);
}

@Override
public void draw(Canvas canvas) {
}

@Override
protected void dispatchDraw(Canvas canvas) {
}

ViewStub 在布局文件中的使用與其他的 View 一致,只是需要增加 layout 屬性:

<ViewStub
    android:id="@+id/ll_draw"
    android:layout_width="@dimen/drawer_width"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    android:layout="@layout/stub_activity_main_drawer" />

當需要使用到 ViewStub 對應的 Views 時,只需要調用 ViewStub#inflate() 方法或將其可見性設置為 VISIBLE 即可。需要注意的是,ViewStub#inflate() 只能調用一次,因為 inflate 之后,ViewStub 會被對應的 Views 替換,ViewStub 會從原來的 Parent 中被移除,如果再次調用 ViewStub 就會拋出異常。

ViewStub stub = (ViewStub) findViewById(R.id.ll_draw);
View drawer = stub.inflate();

布局原則總結

通過以上的分析過程,可以得出一些通用的準則,在 Android UI 布局過程中,遵守這些慣用、有效的布局原則,可以制作出高效且復用性高的 UI。

  1. 盡量多使用 ConstraintLayout、RelativeLayout、LinearLayout
  • 盡量使用 ConstraintLayout
  • 在布局層級相同的情況下,使用 LinearLayout 代替 RelativeLayout
  • 在布局復雜或層級過深時,使用 RelativeLayout 代替 LinearLayout 使界面層級扁平化
  1. 將可復用的組件抽取出來并通過 include 標簽使用
  2. 使用 merge 減少布局的嵌套層級
  3. 使用 ViewStub 加載一些不常用的布局
  4. 盡可能少用 layout_weight
  5. 去除不必要的背景,減少過度繪制
  • 有多層背景重疊的,保留最上層。或者可以統一的使用一個大的背景
  • 對于 Selector 當背景的,可以將 normal 狀態的 color 設置為 @android:color/transparent

參考文章:

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

推薦閱讀更多精彩內容