Android性能调优篇之UI布局优化

作者: 进击的欧阳 | 来源:发表于2017-08-02 21:00 被阅读2722次

    开篇废话

    根据第一篇的规划,趁着周日的空闲时间,认真查阅了UI布局优化的一些知识,在这里总结一下,写这篇文章的最终目的是希望能给从用户最直观的体验部分进行有效的UI优化,使用的方法是从实际操作到原理解说,做到知其然,且知其所以然。

    senduo's blog这是我的个人博客,欢迎访问。。。


    技术详情

    一、Android UI 渲染简述

    我们首先需要知道一个大概是生物领域的一个知识点:

    
    人眼与大脑之间的协作无法感知超过60fps的画面更新。
    
    

    那么对于这个60fps的值是个什么概念呢?举几个例子说明一下,如果我们自己用手快速翻书,纸张哗哗哗的在眼前略过,那个帧率的值大概是12fps,电影胶卷使用的帧率大概是24fps,因为24fps的值可以使人眼感知的是连续性的运动,其实再大点也是可以,但意味着增加了人工成本,需要更多的帧,所以24帧能够做到最少的费用支出(好像有点跑偏了)。所以,我们app的性能标准就是保证60fps这个值,也就代表每一帧,只有1000/60 = 16ms的时间来处理任务。

    Android系统每一次的渲染都是通过底层一个VSYNC信号来触发UI的渲染,间隔时间为16ms,那么我们应该了解为什么需要有UI布局优化这个概念。

    imageimage

    我们在使用app的时候,会发现有多的时候,界面会出现卡顿不流畅的情况,是因为当前这个界面UI的处理超过了16ms,则会占用下一个16ms,这样就导致16ms * 2 都是显示的同一帧,也就是我看到的“卡了”

    imageimage

    二、UI布局优化实操

    了解Android的渲染机制后,下面我们该来分析分析我们的app为什么会出现超过16ms的重绘时间的,大致有以下几点原因:

    1.布局层次不合理
    2.布局存在过度绘制
    

    下面针对以上原因,给出相应的解决方案

    1.使用Hierarchy Viewer工具检测

    对于Hierarchy Viewer工具的使用,可以查看我之前的一篇文章:

    Android性能调优篇之Hierarchy Viewer工具的使用

    优化案例:

    使用LinearLayout嵌套布局版本(嵌套了一层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="wrap_content"
        android:orientation="horizontal">
    
        <ImageView
            android:id="@+id/chat_author_avatar1"
            android:layout_width="@dimen/left_width_height"
            android:layout_height="@dimen/left_width_height"
            android:layout_margin="@dimen/around_margin"
            android:src="@mipmap/ic_launcher"/>
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@string/up_text" />
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@string/down_text"/>
        </LinearLayout>
    
    </LinearLayout>
    

    使用Hierarchy Viewer工具查看的结果是:

    imageimage

    使用RelativeLayout优化布局为:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    
        <ImageView
            android:id="@+id/left_image"
            android:layout_width="@dimen/left_width_height"
            android:layout_height="@dimen/left_width_height"
            android:layout_margin="@dimen/around_margin"
            android:src="@mipmap/ic_launcher"/>
    
    
        <TextView
            android:id="@+id/tvUpText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@id/left_image"
            android:text="@string/up_text" />
    
        <TextView
            android:id="@+id/tvDownText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/tvUpText"
            android:layout_toRightOf="@id/left_image"
            android:text="@string/down_text" />
    </RelativeLayout>
    

    然后使用Hierarchy Viewer工具查看结果为:

    imageimage

    通过两次的结果截图中的有色灯(三个圈圈分别表示measure 、layout、draw的速度)来看,红色(红色的指示灯能够提供给我们一些依据)的已经没有了。一般我们布局编辑按以下规则进行编码:

    1.尽量多使用 ConstraintLayout、RelativeLayout、LinearLayout
    2.尽量使用 ConstraintLayout
    3.在布局的层级相同的情况下,使用 LinearLayout 代替 RelativeLayout
    4.在布局复杂或者层级过深的时候,使用 RelativeLayout 代替 LinearLayout 使界面层级扁平化,降低层级
    

    另外,关于app界面层级的优化,Android Studio给我们提供了一个很好的工具:Lint,通过Lint工具的分析功能,能够检测出一些布局方面的不足,然后方便我们进行优化定位,至于Lint工具的使用步骤,以后会单独生成一篇博客。这里先引用别人的博客:Improve Your Code with Lint

    2.使用include 和merge标签减少复用布局而产生的布局嵌套,使用ViewStub懒加载减少渲染元素

    布局复用的步骤大致为:

    1.创建一个正常的可用布局layout文件A_layout.xml
    2.在需要添加复用布局(A_layout.xml)的当前布局内B_layout.xml,使用include标签
    3.将A_layout.xml的布局文件中的Root View 替换成merge标签,从而减少布局嵌套
    

    第一步:创建一个正常可用的布局layout文件,命名为A_layout.xml

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vetical" >
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="张三" />
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="李四" />
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="王五" />
    
    </LinearLayout>
    

    第二步:使用include标签把复用布局添加到实际需要的布局中(B_layout.xml)去

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">
    
        <include layout="@layout/A_layout"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/hello_world" />
    
    </LinearLayout>
    

    第三步:把第一步中的布局文件中的LinearLayout替换成merge标签(当熟悉之后,可以直接创建一个merge标签的布局文件,第三步可以省略)

    <merge xmlns:android="http://schemas.android.com/apk/res/android">
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="张三" />
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="李四" />
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="王五" />
    
    </merge>
    

    系统在加载这个布局的时候,会自动忽略merge标签,从而将上述的三个TextView直接加入到第二步中的include标签的位置,而并没有增加布局层级

    3.诊断过度绘制,优化过度绘制

    诊断过度绘制

    
    过度绘制:屏幕上某一像素点在一帧中被重复绘制多次
    
    
    分类(根据层度):
    无过度绘制(一个像素只被绘制了一次)      (原色)
    过度绘制x1(一个像素被绘制了两次)        (蓝色)
    过度绘制x2(一个像素被绘制了三次)        (绿色)
    过度绘制x3(一个像素被绘制了四次)        (粉色)
    过度绘制x4+(一个像素被绘制了五次以上)   (红色)
    
    
    
    imageimage

    接着 我们一起来看一下,如何查看我们的App过度绘制的情况

    第一种方案:adb命令行的方式:

    
    打开调试GPU过度绘制
    adb shell setprop debug.hwui.overdraw show
    
    关闭调式GPU过度绘制
    adb shell setprop debug.hwui.overdraw false
    
    

    第二种方案:我们的Android调试机中的开发者选项中开启【调试GPU过度绘制】

    进入我们的调试机器饿开发者选项界面中,往下拉会看到:

    imageimage

    点击后,选择【显示过度绘制区域】

    imageimage

    如果发现我们的app上深红色的色块比较多,那么可能就要注意了,需要进行优化了。

    优化过度绘制

    1. 移除不必要的background

    我们首先根据GPU过度绘制定位到需要优化的布局位置:

    imageimage

    然后定位到代码:

    <?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="wrap_content"
        android:background="@android:color/white"
        android:orientation="horizontal">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/white"
            android:orientation="horizontal">
            <ImageView
                android:id="@+id/chat_author_avatar1"
                android:layout_width="@dimen/left_width_height"
                android:layout_height="@dimen/left_width_height"
                android:layout_margin="@dimen/around_margin"
                android:src="@mipmap/ic_launcher"/>
    
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@android:color/white"
                android:orientation="vertical">
    
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:background="@android:color/white"
                    android:text="@string/up_text" />
    
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:background="@android:color/white"
                    android:text="@string/down_text"/>
            </LinearLayout>
        </LinearLayout>
    
    </LinearLayout>
    

    我们发现,不居中确实出现了好多次的backgound的配置:


    imageimage

    所以移除一些不必要的background

    <?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="wrap_content"
        android:orientation="horizontal">
    
        <ImageView
            android:id="@+id/chat_author_avatar1"
            android:layout_width="@dimen/left_width_height"
            android:layout_height="@dimen/left_width_height"
            android:layout_margin="@dimen/around_margin"
            android:src="@mipmap/ic_launcher"/>
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
    
            android:orientation="vertical">
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@string/up_text" />
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@string/down_text"/>
        </LinearLayout>
    
    </LinearLayout>
    

    运行之后为:


    imageimage

    明显有改善了,所以在我们实际生产中,需要非常有耐心的去不断诊断,不断去调试,有些时候我们使用ImageView的时候,可能会给它设置一个background,在代码中,还有可能给它设了一个imageDrawable,从而发生过度绘制的情况,切记切记。
    解决方案是把背景图和真正加载的图片都通过imageDrawable方法进行设置。

    2. clipRect和clipPath方法的妙用

    clipRect的功能可以理解为在一个大的画布中,用一些大小可变的矩形一个一个来裁切,在某一个矩形内,绘制想要绘制的图形,超出的不进行绘制,当我们的app这样进行写自定义View的时候,可以避免view与view之间的叠加,从而产生同一个像素点被绘制多次的情况,原理就是这样,贴一个Hongyang大神的重写onDraw方法:

    @Override
    protected void onDraw(Canvas canvas)
    {
    
        super.onDraw(canvas);
    
        canvas.save();
        canvas.translate(20, 120);
        for (int i = 0; i < mCards.length; i++)
        {
            canvas.translate(120, 0);
            canvas.save();
            if (i < mCards.length - 1)
            {
                canvas.clipRect(0, 0, 120, mCards[i].getHeight());
            }
            canvas.drawBitmap(mCards[i], 0, 0, null);
            canvas.restore();
        }
        canvas.restore();
    
    }
    

    干货总结

    文章的内容参考了Hongyang大神的博客:Android UI性能优化实战 识别绘制中的性能问题

    关于UI布局优化的内容,其实存在很多细节,更多的情况,还是得依靠我们平时对于UI布局优化的积累与经验去进行优化。再次总结几点经验:

    1.将可重复使用的布局通过include标签与merge标签搭配进行使用
    
    2.尽量少添加不必要的背景,减少过度绘制
    
    3.布局能扁平化的扁平化,尽量不要增加布局层级
    
    4.适当的时侯某些控件使用懒加载ViewStub,但需要牢记它的坑
    
    5.使用Hierarchy View 工具或者Lint工具来进行app的检测
    

    相关文章

      网友评论

        本文标题:Android性能调优篇之UI布局优化

        本文链接:https://www.haomeiwen.com/subject/wiwylxtx.html