天天看點

Android性能優化-過度繪制

文章目錄

    • 背景
    • 過度繪制
      • 補充
      • 檢測布局中的背景重疊
      • 檢測視圖層級
      • Hierarchy Viewer工具檢測
      • clipRect 和 quickReject 方法

背景

之前我們的項目開發周期,從兩周發一個版本,變成一周發一版本,這種快速疊代的節奏持續了将近一年半。平時開發,重心都放在了業務之上,很難有很多的時間去分析一些複雜業務多帶來的性能問題,導緻代碼越來越沉重(比如:一個Fragment頁面的代碼到了3千多行。),而且頁面渲染速度和幀率都大大下降,出現卡頓,嚴重影響使用者的體驗。當時這種問題已經發展到了很嚴重的地步,如果再一直這樣持續下去,将可能導緻使用者流失,是以性能問題是必須解決的。

大概半年之前,我們開始對業務和架構進行重新梳理,對項目做一些重構和優化。我參與到了這個優化項目中,主要負責對過度渲染,過度繪制,方法耗時,記憶體優化方面的優化,下面将對這幾點進行一些總結。

過度繪制

去除過度繪制主要從三方面入手:

  1. 移除布局中不必要的背景
  2. 使視圖層級扁平化
  3. 降低透明度

具體如何分析和去除過度繪制,可以檢視我之前的部落格關于過度繪制和渲染的介紹。

補充

檢測布局中的背景重疊

關于背景重疊引起的過度繪制,可以從統計View背景重疊的次數,來做具體的優化。View是一個樹形結構,可以對View樹進行周遊,得到過度繪制的View路徑:

/**
     * 最小次數
     */
    private static final int NUM = 3;

    /**
     * 測試過度繪制的View路徑
     *
     * @param view 根View節點
     */
    public static void testBackgroundOverdraw(View view) {
        Map<ArrayList<View>, Integer> result = new LinkedHashMap<>();

        ArrayList<View> list = new ArrayList<>();

        findBackgroundOverdrawPath(view, result, list, NUM);

        Log.d(TAG, "背景過度繪制大于" + NUM + "的布局的個數:" + result.size());

        Iterator<Map.Entry<ArrayList<View>, Integer>> iterator = result.entrySet().iterator();
        int index = 0;
        while (iterator.hasNext()) {
            Map.Entry<ArrayList<View>, Integer> entry = iterator.next();
            ArrayList path = entry.getKey();
            Integer num = entry.getValue();
            Log.d(TAG, "布局[" + index++ + "]深度:" + path.size() + ";次數:" + num + ";布局視圖:" + path.toString());
        }
    }


    /**
     * @param node   根View節點
     * @param result 過度繪制路徑集合
     * @param list   存儲臨時路徑
     * @param target 過度繪制次數
     */
    private static void findBackgroundOverdrawPath(View node, Map<ArrayList<View>, Integer> result, ArrayList<View> list, int target) {
        if (node == null) return;
        list.add(node);

        if (!(node instanceof ViewGroup)) {
            int count = 0;
            for (View view : list) {
                if (view.getBackground() != null) count++;
            }
            if (count >= target)
                result.put(list, count);
        } else {
            ViewGroup viewGroup = (ViewGroup) node;
            for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {
                findBackgroundOverdrawPath(viewGroup.getChildAt(i), result, new ArrayList<View>(list), target);
            }
        }
    }
           

例如,某個Activity的布局:

<?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"
    android:background="@android:color/holo_green_light"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_blue_light"
        android:text="Hello World!"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_red_light"
        android:orientation="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:background="@android:color/holo_blue_light"
            android:text="Hello World!" />

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:background="@android:color/holo_orange_light"
                android:text="Hello World!" />
        </FrameLayout>

    </LinearLayout>

</android.support.constraint.ConstraintLayout>
           

統計結果:

11-04 21:12:40.371 21520-21520/com.example.wangjiang.after D/UITestUtil: 背景過度繪制大于3的布局的個數:3
11-04 21:12:40.371 21520-21520/com.example.wangjiang.after D/UITestUtil: 布局[0]深度:7;次數:3;布局視圖:[com.android.internal.policy.PhoneWindow$DecorView{dc28997 V.E...... R.....ID 0,0-720,1280}, android.widget.LinearLayout{38ac784 V.E...... ......ID 0,0-720,1280}, android.widget.FrameLayout{ef95b6d V.E...... ......ID 0,36-720,1280}, android.support.v7.widget.ActionBarOverlayLayout{d5553a2 V.E...... ......ID 0,0-720,1244 #7f070030 app:id/decor_content_parent}, android.support.v7.widget.ContentFrameLayout{e1c2833 V.E...... ......ID 0,112-720,1244 #1020002 android:id/content}, android.support.constraint.ConstraintLayout{aed7f0 V.E...... ......ID 0,0-720,1132}, android.support.v7.widget.AppCompatTextView{3dd4169 V.ED..... ......ID 286,0-435,38}]
11-04 21:12:40.371 21520-21520/com.example.wangjiang.after D/UITestUtil: 布局[1]深度:8;次數:4;布局視圖:[com.android.internal.policy.PhoneWindow$DecorView{dc28997 V.E...... R.....ID 0,0-720,1280}, android.widget.LinearLayout{38ac784 V.E...... ......ID 0,0-720,1280}, android.widget.FrameLayout{ef95b6d V.E...... ......ID 0,36-720,1280}, android.support.v7.widget.ActionBarOverlayLayout{d5553a2 V.E...... ......ID 0,0-720,1244 #7f070030 app:id/decor_content_parent}, android.support.v7.widget.ContentFrameLayout{e1c2833 V.E...... ......ID 0,112-720,1244 #1020002 android:id/content}, android.support.constraint.ConstraintLayout{aed7f0 V.E...... ......ID 0,0-720,1132}, android.widget.LinearLayout{f5907ee V.E...... ......ID 0,528-720,604}, android.support.v7.widget.AppCompatTextView{967148f V.ED..... ......ID 285,0-434,38}]
11-04 21:12:40.371 21520-21520/com.example.wangjiang.after D/UITestUtil: 布局[2]深度:9;次數:4;布局視圖:[com.android.internal.policy.PhoneWindow$DecorView{dc28997 V.E...... R.....ID 0,0-720,1280}, android.widget.LinearLayout{38ac784 V.E...... ......ID 0,0-720,1280}, android.widget.FrameLayout{ef95b6d V.E...... ......ID 0,36-720,1280}, android.support.v7.widget.ActionBarOverlayLayout{d5553a2 V.E...... ......ID 0,0-720,1244 #7f070030 app:id/decor_content_parent}, android.support.v7.widget.ContentFrameLayout{e1c2833 V.E...... ......ID 0,112-720,1244 #1020002 android:id/content}, android.support.constraint.ConstraintLayout{aed7f0 V.E...... ......ID 0,0-720,1132}, android.widget.LinearLayout{f5907ee V.E...... ......ID 0,528-720,604}, android.widget.FrameLayout{ec1831c V.E...... ......ID 0,38-720,76}, android.support.v7.widget.AppCompatTextView{1ff8b25 V.ED..... ......ID 285,0-434,38}]

           

上面統計了布局背景重疊超3次以上的布局路徑,3次一個,4次兩個,根節點是從DecorView開始的。當然,也可以根據個人需要來收集相應的資訊。

檢測視圖層級

同理,布局層級嵌套次數的統計,也可以通過同樣的方式:

/**
     * 測試布局層級的View路徑
     *
     * @param view 根View節點
     */
    public static void testLayoutHierarchy(View view) {
        ArrayList<ArrayList<View>> result = new ArrayList<>();

        ArrayList<View> list = new ArrayList<>();

        findLayoutHierarchyPath(view, result, list, NUM);

        Log.d(TAG, "布局層級嵌套大于" + NUM + "的布局的個數:" + result.size());

        int index = 0;
        for (ArrayList path : result)
            Log.d(TAG, "布局[" + index++ + "]深度:" + path.size() + ";嵌套次數:" + (path.size() - 1) + ";布局視圖:" + path.toString());

    }


    /**
     * @param node   根View節點
     * @param result 布局層級路徑集合
     * @param list   存儲臨時路徑
     * @param target 布局層級嵌套的次數
     */
    private static void findLayoutHierarchyPath(View node, ArrayList<ArrayList<View>> result, ArrayList<View> list, int target) {
        if (node == null) return;
        list.add(node);

        if (!(node instanceof ViewGroup)) {
            int count = list.size();
            if (--count >= target)
                result.add(list);
        } else {
            ViewGroup viewGroup = (ViewGroup) node;
            for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {
                findLayoutHierarchyPath(viewGroup.getChildAt(i), result, new ArrayList<View>(list), target);
            }
        }
    }
           

統計結果:

11-04 21:43:53.786 23207-23207/com.example.wangjiang.after D/UITestUtil: 布局層級嵌套大于3的布局的個數:2
11-04 21:43:53.786 23207-23207/com.example.wangjiang.after D/UITestUtil: 布局[0]深度:4;嵌套次數:3;布局視圖:[android.support.v7.widget.ContentFrameLayout{e1c2833 V.E...... ......ID 0,112-720,1244 #1020002 android:id/content}, android.support.constraint.ConstraintLayout{aed7f0 V.E...... ......ID 0,0-720,1132}, android.widget.LinearLayout{f5907ee V.E...... ......ID 0,528-720,604}, android.support.v7.widget.AppCompatTextView{967148f V.ED..... ......ID 285,0-434,38}]
11-04 21:43:53.786 23207-23207/com.example.wangjiang.after D/UITestUtil: 布局[1]深度:5;嵌套次數:4;布局視圖:[android.support.v7.widget.ContentFrameLayout{e1c2833 V.E...... ......ID 0,112-720,1244 #1020002 android:id/content}, android.support.constraint.ConstraintLayout{aed7f0 V.E...... ......ID 0,0-720,1132}, android.widget.LinearLayout{f5907ee V.E...... ......ID 0,528-720,604}, android.widget.FrameLayout{ec1831c V.E...... ......ID 0,38-720,76}, android.support.v7.widget.AppCompatTextView{1ff8b25 V.ED..... ......ID 285,0-434,38}]

           

布局也是上面的布局,統計到布局層級嵌套超過3層的有2個,根節點是從android.R.id.content 開始的。

Hierarchy Viewer工具檢測

Hierarchy Viewer可以很直接的呈現布局的層次關系,視圖元件的各種屬性。 我們可以通過紅,黃,綠三種不同的顔色來區分布局的Measure,Layout,Executive的相對性能表現如何。

注意:Hierarchy Viewer已經被抛棄了。如果您使用的是Android Studio 3.1或更高版本,那麼應該使用Layout Inspector在運作時檢查應用程式的視圖層次結構。要了解應用程式布局的渲染速度,請使用此部落格文章中描述的Window.OnFrameMetricsAvailableListener。

clipRect 和 quickReject 方法

Canvas類提供了clipRect 和 quickReject 方法:

  • clipRect:可以指定一塊矩形區域,隻有在這個區域内才會被繪制,其他的區域會被忽視。
  • quickReject:判斷是否沒和某個矩形相交,進而跳過那些非矩形區域内的繪制操作。

clipRect 和 quickReject 方法主要是用來避免在自定義View中繪制重疊的區域。

繼續閱讀