Android性能优化实战(二)----界面布局优化

来源:互联网 发布:python爬虫书籍pdf 编辑:程序博客网 时间:2024/05/21 14:02

App界面布局是用户能体验到应用性能好坏最直接的方式,如果布局写得不好,App就容易卡顿,严重影响用户体验。通过这篇博客,来学习总结优化Gallery时用到的View布局优化方法。

优化布局层次结构

我们知道,Android View的绘制分为三个过程:measure、layout和draw,首先绘制的父类布局ViewGroup,绘制完父类布局后再对ViewGroup里面的子View绘制,如果你的app布局层次复杂,就会降低绘制的效率。Android SDK自带一个UI性能检测工具 Hierarchy Viewer,我们可以从SDK的tools目录找到该工具,也可以在Android Studio的Android Device Monitor中找到。

这里写图片描述

Tree View界面就为我们直观的展示了当前Activity的View树结构。点击Profile Node,将会重新绘制View Tree,点击某个节点,可以查看绘制该View时的具体信息。

这里写图片描述

这里我们主要关注下面的三个圆圈,从左到右依次,代表View的measure, layout和draw的性能,不同颜色代表不同的性能等级:
1、 绿: 表示该View的此项性能比该View Tree中的至少一半以上的View都要快;
2、黄: 表示该View的此项性能比该View Tree中的至少一半以上的View都要慢;
3、红: 表示该View的此项性能是View Tree中最慢的。
不过以上的指标都是相对于这个View所在的View Tree来比较的,并不是绝对的,也就是说红色并不意味性能差。不过红色的节点View可以会存在性能问题:
1、如果该节点是父节点,而且只有几个子节点,虽然可能实际体验起来并没有问,我们最好借助Systrace或者Traceview工具来获取更多的信息分析一下,看是否存在问题;
2、如果一个父节点有许多的子节点,并且Measure阶段呈现为红色,则需要观察下子节点的绘制情况;
3、如果视图中的根节点,Measure阶段为红色,Layout阶段为红色,Draw阶段为黄色,这个是比较常见的,因为这个节点是所有其它视图的父类;
4、如果一个有很多个View的子节点在Draw阶段是红色的,这明显是有问题的,需要检查一下代码里面的onDraw方法,是否调用正确。
对于父类ViewGroup,我们最常用到的是RelativeLayout和LinearLayout,我们应该如何选择?源码中查看它们的绘制过程:
RelativeLayout的onMeasure

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    ......    View[] views = mSortedHorizontalChildren;        int count = views.length;        for (int i = 0; i < count; i++) {            View child = views[i];            if (child.getVisibility() != GONE) {                LayoutParams params = (LayoutParams) child.getLayoutParams();                int[] rules = params.getRules(layoutDirection);                applyHorizontalSizeRules(params, myWidth, rules);                measureChildHorizontal(child, params, myWidth, myHeight);                if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {                    offsetHorizontalAxis = true;                }            }        }        views = mSortedVerticalChildren;        count = views.length;        final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;        for (int i = 0; i < count; i++) {            final View child = views[i];            if (child.getVisibility() != GONE) {                final LayoutParams params = (LayoutParams) child.getLayoutParams();                applyVerticalSizeRules(params, myHeight, child.getBaseline());                measureChild(child, params, myWidth, myHeight);                if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {                    offsetVerticalAxis = true;                }                if (isWrapContentWidth) {                    if (isLayoutRtl()) {                        if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {                            width = Math.max(width, myWidth - params.mLeft);                        } else {                            width = Math.max(width, myWidth - params.mLeft - params.leftMargin);                        }                    } else {                        if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {                            width = Math.max(width, params.mRight);                        } else {                            width = Math.max(width, params.mRight + params.rightMargin);                        }                    }                }                if (isWrapContentHeight) {                    if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {                        height = Math.max(height, params.mBottom);                    } else {                        height = Math.max(height, params.mBottom + params.bottomMargin);                    }                }                if (child != ignore || verticalGravity) {                    left = Math.min(left, params.mLeft - params.leftMargin);                    top = Math.min(top, params.mTop - params.topMargin);                }                if (child != ignore || horizontalGravity) {                    right = Math.max(right, params.mRight + params.rightMargin);                    bottom = Math.max(bottom, params.mBottom + params.bottomMargin);                }            }        }    ......}

LinearLayout的onMeasure:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        if (mOrientation == VERTICAL) {            measureVertical(widthMeasureSpec, heightMeasureSpec);        } else {            measureHorizontal(widthMeasureSpec, heightMeasureSpec);        }    }

从RelativeLayout源码的13行和31行分别调用measureChildHorizontal()和 measureChild(),也就是说RelativeLayout会让子View调用2次onMeasure;而LinearLayout 则简单得很多,只会调用子View1次onMeasure,不过查看一下measureVertical或者measureHorizontal会发现,如果在有weight这个属性的时候,LinearLayout也会让子View调用两次onMeasure。这样看来,在没有weight属性的时候,LinearLayout的花销确实要比RelativeLayout的要少,但是如果在嵌套很多子View的情况下,例如:

这里写图片描述

上图中为了在垂直(水平)LinearLayout中再嵌入一个水平(垂直)的布局,只能在嵌入一个LinearLayout,而如果使用RelativeLayout作为父容器的话,明显可以减少一层布局层数:

这里写图片描述

所以,在不响应层级深度的情况下,使用Linearlayout而不是RelativeLayout。

使用布局标签include、merge和viewStub

<include> 标签

有时候我们经常需要重复用到同一布局,如果总是复制粘贴,未免有些麻烦。其实Android当然也已经充分考虑到开发者的这一需求,为我们提供了<include>标签,这个标签的使用很简单,例如:

    <android.support.v7.widget.ContentFrameLayout        android:id="@+id/content"        android:layout_width="match_parent"        android:layout_height="match_parent">        <include layout="@layout/toolbar_layout"/>        <include layout="@layout/toolbar_shadow"/>        <include layout="@layout/album_page_layout"/>    </android.support.v7.widget.ContentFrameLayout>

此外我们还可以更改<include>标签当中的属性:

        <include layout="@layout/album_page_layout"            android:layout_height="match_parent"            android:layout_width="match_parent"/>

这样,以后我们要修改布局文件,只需要修改一处,就可以一劳永逸了。

<merge> 标签

上面我们说道,应该尽量减少我们的布局层次,提高View的绘制效率,<merge> 应运而生,它的主要作用是为了防止在引用布局文件时产生多余的布局嵌套。就拿我们最常用的setContentView(R.layout.activity_main)这个方法来说吧,其实最终系统是把activity_layout这个布局放到id为content的FrameLayout中去,此时,如果我们的activity_main这个布局的根节点也是一个FrameLayout,就产生了一个多余的层次:

这里写图片描述

此时我们就可以用<merge> 来去除多余的嵌套:

<merge xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:fitsSystemWindows="true">    <LinearLayout        android:id="@+id/mainPanel"        android:layout_width="wrap_content"        android:layout_height="match_parent"        android:orientation="vertical" >            <com.tct.gallery3d.filtershow.crop.CropView                android:id="@+id/cropView"                android:layout_width="match_parent"                android:layout_height="wrap_content" />            <ProgressBar                android:id="@+id/loading"                style="@android:style/Widget.Holo.ProgressBar.Large"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:layout_gravity="center"                android:indeterminate="true"                android:indeterminateOnly="true"                android:background="@android:color/transparent" />    </LinearLayout></merge>

来看一下修改后的效果,只有一个FrameLayout了:

这里写图片描述

<ViewStub> 标签

在很多时候,会在运行时动态地显示某个布局,通常的做法是设置该布局invisible或者gone,然后在代码中动态的更改它的可见性。虽然把View的初始状态设置为invisible或者gone,但是在加载布局的时候View仍然会被inflate,浪费资源。那么我们如何才能让这些不常用的元素仅在需要时才去加载呢?Android为此提供了一种非常轻量级的控件ViewStub。ViewStub虽说也是View的一种,但是它没有大小,没有绘制功能,也不参与布局,资源消耗非常低,将它放置在布局当中基本可以认为是完全不会影响性能的.

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

在使用时候:

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

但是有一点需要注意的是,ViewStub不支持<merge> 标签。

尽量避免Overdraw

Overdraw(过度绘制)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的UI结构里面,如果不可见的UI也在做绘制的操作,会导致某些像素区域被绘制了多次。这样就会浪费大量的CPU以及GPU资源。为了获取更好的性能,我们应该尽量避免过度绘制。为了查看我们的app界面是否过度绘制,我们可以通过手机设置里面的开发者选项,打开Show GPU Overdraw的选项,观察UI上的Overdraw情况
这里写图片描述
这里写图片描述

颜色越深,代表过度绘制的情况越严重。下面,我们通过这个工具来查看一下Gallery的绘制情况。可以看到,在我司的Gallery界面上,过度绘制还是很严重的。通过查看xml文件,我们发现,我们的主题原本就设置了背景,但是在子布局上又重复设置了背景,这样在onDraw的时候就需要多次绘制布局的背景:
这里写图片描述

    <style name="Theme.Gallery" parent="Theme.GalleryBase">        <item name="android:displayOptions"></item>        <item name="android:windowContentOverlay">@null</item>        <item name="android:actionBarStyle">@style/Holo.ActionBar</item>        <item name="android:windowBackground">@android:color/black</item>        <item name="android:colorBackground">@null</item>        <item name="android:colorBackgroundCacheHint">@null</item>    </style>
<FrameLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:background="@android:color/transparent"    android:orientation="vertical">        android:background="@drawable/photopage_actionbar_background"/>    <RelativeLayout        xmlns:android="http://schemas.android.com/apk/res/android"        xmlns:app="http://schemas.android.com/apk/res-auto"        android:id="@+id/bottom_bar_background"        android:layout_width="match_parent"        android:layout_height="160dp"        android:background="@drawable/bottom_control_background"        android:layout_gravity="center_horizontal|bottom"/></FrameLayout>

我们在OnCreate()中将background设置为空之后再来看一下界面:

    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        Log.d(TAG, "onCreate");        getWindow().setBackgroundDrawable(null);    }

修改之后,颜色明显变淡了

这里写图片描述

通常,优化绘制可以通过以下方法:
移除Window默认的Background
移除XML布局文件中非必需的Background
按需显示占位背景图片

以上,就是我在优化Gallery布局时所用到的一些方法,此外Android Studio上为卡发着提供了一个性能检测工具Lint,这样我们就不用手动去查找那个布局是否存在优化的空间,其实Lint的功能远不止这些,详情请看Android官网上的介绍:https://developer.android.google.cn/studio/write/lint.html

原创粉丝点击