• Android布局优化(一)LayoutInflate — 从布局加载原理说起
  • Android布局优化(二)优雅获取界面布局耗时
  • Android布局优化(三)使用AsyncLayoutInflater异步加载布局
  • Android布局优化(四)X2C — 提升布局加载速度200%
  • Android布局优化(五)绘制优化—避免过度绘制

目录

前言

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

如何监控应用渲染速度

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

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

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

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

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

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

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

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

如何检测过度绘制

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

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

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

如何避免过度绘制

移除window的背景

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

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

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

  1. 在xml中设置
 @null

通过代码设置

 getWindow().setBackgroundDrawable(null);

移除控件中不需要的背景

例子:

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

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

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

减少透明度的使用

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

使用ConstraintLayout减少布局层级

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

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

使用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"?>    
  1. 有时候我们会通 过include标签来提高布局的复用性,如果layout_include_xx.xml的布局和其父布局使用的是同一个布局类型,如线性布局等。这时候就可以在layout_include_xx.xml中使用merge标签来减少布局层级

使用ViewStub标签延迟加载

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

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

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

ViewStub标签使用注意点:

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

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

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

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

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

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);        }    }

显示过度绘制区域:

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

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

那么,解决办法也很简单,对于前面的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();    }}

优化后的效果图如下:

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

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

总结

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

更多相关文章

  1. Android(安卓)性能分析案例
  2. Android实现书籍翻页效果--扩展版
  3. 自己对android开机速度优化的一点理解
  4. [置顶] Android学习博客和文章存档
  5. Android(安卓)常用的画图方法
  6. Android(安卓)性能优化的一些方法
  7. android 性能优化
  8. Android补间动画原理介绍
  9. Android(安卓)性能优化系列总篇 (五)

随机推荐

  1. Android通过HTTP协议实现多线程下载
  2. Android 4.0
  3. 使用Android自带Gallery组件实现CoverFlo
  4. 如何android多Activity间共享数据 (extend
  5. android应用发短信
  6. android上用opengl画线
  7. android JNI demo
  8. Android 8.1 沉浸式状态栏
  9. android CoordinatorLayout里viewpager占
  10. 在Flutter的项目中AndroidX Compatibilit