美文网首页Android开发Android开发经验谈
02 性能优化-布局优化-避免过度绘制

02 性能优化-布局优化-避免过度绘制

作者: 凤邪摩羯 | 来源:发表于2021-03-23 09:13 被阅读0次

    1 目录

    image

    2 前言

    本系列的前面几篇文章我们介绍了布局加载的原理及优化,布局加载完成后(生成VIew对象)就要进行视图绘制,我们知道,android要求每帧的绘制时间不超过16ms,不然就会导致丢帧及应用卡顿。所以本文将会介绍一些布局绘制优化技巧

    3 如何监控应用渲染速度

    点击设置—>开发人员选项—>监控—>GPU呈现模式分析,然后选择 在屏幕上显示为条形图 即可以看到一个图表,如下图所示

    image

    1.沿水平轴的每个竖条都代表一个帧,每个竖条的高度表示渲染该帧所花的时间(单位:毫秒)
    2.水平绿线表示 16 毫秒。 要实现每秒 60 帧,代表每个帧的竖条需要保持在此线以下。 当竖条超出此线时,可能会使动画出现暂停

    再来看下每个竖条的颜色代表什么意思:

    image

    4 分析从哪些方向进行绘制优化

    从GPU呈现模式分析可以看出来,我们能够进行优化的点主要就是测量、布局、绘制、动画和输入处理

    1. 测量、布局、绘制过程都会存在自顶而下遍历过程,所以如果布局的层级过多,这会占用额外的CPU资源
    2. 当屏幕上的某个像素在同一帧的时间内被绘制了多次(Overdraw),这会浪费大量的CPU以及GPU资源
    3. 在绘制过程,也就是onDraw()方法内,我们应该尽量避免局部对象的创建,因为onDraw()方法在绘制过程中会多次调用,大量的局部变量可能会造成内存抖动
    4. 合理使用动画,这个本章不做讨论,有兴趣的可以自己了解动画的相关知识
    5. 不应该在Event响应的回调中做耗时操作

    总结下来视图绘制优化主要要解决的问题就是:

    减少view树层级,要宽而浅,避免窄而深

    5 如何检测过度绘制

    点击设置—>开发人员选项—>硬件—>调试GPU过度绘制,然后选择 显示过度绘制区域 即可以看到一个图表,如下图所示

    image

    再来看下每种颜色代表什么意思:

    image

    有些过度绘制是无法避免的。但是在优化界面时,应该尽量让大部分的界面显示为原色(即无过度绘制)或者为蓝色(仅有 1 次过度绘制)。如果出现粉色或者红色,应该查看代码看看能否尽量避免

    6 如何避免过度绘制

    6.1 移除window的背景

    一般情况下我们的AppTheme都默认带会有windowBackground

    <style name="AppTheme" parent="Theme.AppCompat.Light">
        ...
        <item name="android:windowBackground">@color/background_material_light</item>
            ...
    </style>
    
    

    但是这个windowBackground大部分清洁下都是没有什么意义的,因为我们往往都会在布局文件中设置我们当前view的背景颜色。如果我们同时设置了windowBackground和布局文件中的background,那就会出现两次绘制,这显然是没有什么意义的,因为最终用户看到的颜色还是以background为准

    我们可以通过下面两个方法来解决这个问题

    1. 在xml中设置
     <item name="android:windowBackground">@null</item>
    
    

    通过代码设置

     getWindow().setBackgroundDrawable(null);
    
    

    6.2 移除控件中不需要的背景

    例子:

    1. 列表页(RecyclerView) 与 其内子控件(Item)的背景相同,故可移除子控件(Item)布局中的背景
    2. 对于1个ViewPager+多个 Fragment 组成的首页界面,若每个Fragment 都设有背景色,即 ViewPager 则无必要设置,可移除

    所以对于控件背景颜色的设置基本可以归纳为以下两个原则:

    1. 对于子控件,如果其背景颜色跟父布局一致,那么就不用再给子控件添加背景了
    2. 如果子控件背景五颜六色,且能够完全覆盖父布局,那么父布局就可以不用添加背景了

    6.3 减少透明度的使用

    对于不透明的view,只需要渲染一次即可把它显示出来。但是如果这个view设置了alpha值,则至少需要渲染两次。这是因为使用了alphaview需要先知道混合view的下一层元素是什么,然后再结合上层的view进行Blend混色处理。透明动画、淡入淡出和阴影等效果都涉及到某种透明度,这就会造成了过度绘制。可以通过减少渲染这些透明对象来改善过度绘制。比如:在TextView上设置带透明度alpha值的黑色文本可以实现灰色的效果。但是,直接通过设置灰色的话能够获得更好的性能

    6.4 使用ConstraintLayout减少布局层级

    ConstraintLayout,可以翻译为约束布局,在2016年Google I/O 大会上发布。ConstraintLayout相比RelativeLayout,其性能更好,也更容易使用。连官方的hello world都用ConstraintLayout来写了。所以极力推荐使用ConstraintLayout来编写布局

    关于ConstraintLayout如何使用,推荐一篇文章讲的非常详细:https://www.jianshu.com/p/17ec9bd6ca8a,所以这里就不过多介绍了。当你熟练使用它之后,相信我,你再也不想用其他布局了!

    6.5 使用merge标签减少布局层级

    我们通过两个例子来认识merge标签

    1. 自定义view时使用merge标签

    比如我们现在要写一个自定义viewGroup继承自ConstraintLayout

    public class MyViewGroup extends RelativeLayout {
        public MyView(Context context) {
            this(context, null, 0);
        }
    
        public MyView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            initView();
        }
    
        public void initView() {
            LayoutInflater.from(getContext()).inflate(R.layout.layout_my_view, this, true);
        }
    }
    
    

    我们通过LayoutInflater将XML加载出view并添加到这个自定义view的根布局中,这时候我们的XML文件就可以这么写。我们在根布局中使用了merge标签,就代表这个xml文件的根布局就是其parent,也就是我们上面的MyViewGroup,这样相比在根布局中使用RelativeLayout就减少了一个布局层级

    这里有一个细节需要注意:当我们使用merge标签时,如果我们希望在Design窗口中实时预览布局效果,我们需要使用 tools:parentTag="android.widget.RelativeLayout"来告诉AndroidStudio你的父布局是什么

    <?xml version="1.0" encoding="utf-8"?>
    <merge xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:parentTag="android.widget.RelativeLayout">
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="hello world" />
    
    </merge>
    
    
    1. 有时候我们会通 过include标签来提高布局的复用性,如果layout_include_xx.xml的布局和其父布局使用的是同一个布局类型,如线性布局等。这时候就可以在layout_include_xx.xml中使用merge标签来减少布局层级

    6.6 使用ViewStub标签延迟加载

    ViewStub是一个不可见的View类,用于在运行时按需懒加载资源,只有在代码中调用了viewStub.inflate()或者viewStub.setVisible(View.visible)方法时才内容才变得可见。这里需要注意的一点是,当ViewStubinflate到parent时,ViewStub就被remove掉了,即当前view hierarchy中不再存在ViewStub,而是使用对应的layout视图代替

    <ViewStub
            android:id="@+id/view_stub"
            android:layout="@layout/layout_error_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
    

    通常用于不常使用的控件,如

    • 网络请求失败的提示
    • 列表为空的提示
    • 新内容、新功能的引导,因为引导基本上只显示一次
    • 又或者我们写了一个通用的自定义 View,但其中部分子 View 只在部分情况下才显示

    ViewStub标签使用注意点:

    1. ViewStub标签不支持merge标签。因此这有可能导致加载出来的布局存在着多余的嵌套结构,具体如何去取舍就要根据各自的实际情况来决定了

    2. ViewStubinflate只能被调用一次,第二次调用会抛出异常

    3. 虽然ViewStub是不占用任何空间的,但是每个布局都必须要指定layout_widthlayout_height属性,否则运行就会报错

    6.7 减少自定义View的过度绘制,使用clipRect()

    下面我们自定义一个View用来显示多张重叠的图片,效果图如下:

    image

    onDraw()方法也很简单,就是遍历所有图片,然后绘制出来:

        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            for (int i = 0; i < imgs.length; i++) {
                canvas.drawBitmap(imgs[i], i * 100, 0, mPaint);
            }
        }
    
    

    显示过度绘制区域:

    image

    过度绘制比较严重,那么如何解决?

    我们先来分析一下为什么会出现过度绘制:以第一张图为例,上面的代码会把整张图都绘制出来了,第二张在第一张上面继续绘制,这就造成了过度绘制

    那么,解决办法也很简单,对于前面的n-1张图,我们只需要绘制一部分即可,对于最后一张才绘制完整的。

    Canvas中的clipRect()方法能够设置一个裁剪矩形,只在这个矩形区域内的内容才能够绘制出来

    优化后的代码如下:

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    
        for (int i = 0; i < imgs.size(); i++) {
            canvas.save();
            if (i < imgs.size() - 1) {
                //前面的n-1张图,只裁剪一部分
                canvas.clipRect(i * 100, 0, (i + 1) * 100, imgs.get(i).getHeight());
            } else if (i == imgs.size() - 1) {
                //最后一张,完整的
                canvas.clipRect(i * 100, 0, i * 100 + imgs.get(i).getWidth(), imgs.get(i).getHeight());
            }
            canvas.drawBitmap(imgs.get(i), i * 100, 0, mPaint);
            canvas.restore();
        }
    }
    
    

    优化后的效果图如下:

    image

    所有区域都是蓝色的,即只有1次过度绘制。

    Canvas除了clipRect()方法外,还有clipPath()等方法,优化时选择合理的方法去裁剪即可

    7 总结

    布局加载优化主要从IO反射为突破口,也可以通过异步加载从侧面环境这个问题。而布局绘制优化致力于解决过度绘制问题。本系列文章(布局优化)到此就结束了,希望对你有所帮助

    相关文章

      网友评论

        本文标题:02 性能优化-布局优化-避免过度绘制

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