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

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

移除控件中不需要的背景

例子:

  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继承自RelativeLayout

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标签来减少布局层级

使用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属性,否则运行就会报错

减少自定义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反射为突破口,也可以通过异步加载从侧面环境这个问题。而布局绘制优化致力于解决过度绘制问题。本系列文章(布局优化)到此就结束了,希望对你有所帮助

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容