Android性能优化-过度绘制

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wangjiang_qianmo/article/details/83714736

背景

之前我们的项目开发周期,从两周发一个版本,变成一周发一版本,这种快速迭代的节奏持续了将近一年半。平时开发,重心都放在了业务之上,很难有很多的时间去分析一些复杂业务多带来的性能问题,导致代码越来越沉重(比如:一个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 开始的。

扫描二维码关注公众号,回复: 5609238 查看本文章

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中绘制重叠的区域。

猜你喜欢

转载自blog.csdn.net/wangjiang_qianmo/article/details/83714736