绘制与布局优化

1. 概述

现在的APP一些视觉效果都很炫,往往在一个界面上堆叠了很多视图,这很容易出现一些性能的问题,严重的话甚至会造成卡顿。因此,我们在开发时必须要平衡好设计效果和性能的问题。

本文主要讲解如何对视图和布局进行优化:包括如何避免过度绘制,如何减少布局的层级,如何使用ConstraintLayout等等。

2. 过度绘制(Overdraw)

2.1 什么是过度绘制?

过度绘制(Overdraw)指的是屏幕上的某个像素在同一帧的时间内被绘制了多次。

举个例子:在多层次的UI结构里面,如果不可见的UI也进行绘制操作,那么就会造成某些像素区域被绘制了多次。这会浪费大量的CPU以及GPU资源。这是我们需要避免的。

2.2 如何检测过度绘制

Android手机上面的开发者选项提供了工具来检测过度绘制,可以按如下步骤来打开:

开发者选项->调试GPU过度绘制->显示过度绘制区域

如下图所示:

可以看到,界面上出现了一堆红绿蓝的区域,我们来看下这些区域代表什么意思:

需要注意的是,有些过度绘制是无法避免的。因此在优化界面时,应该尽量让大部分的界面显示为真彩色(即无过度绘制)或者为蓝色(仅有 1 次过度绘制)。尽量避免出现粉色或者红色。

2.3 过度绘制优化

可以采取以下方案来减少过度绘制:

1.移除布局中不需要的背景
2.将layout层级扁平化
3.减少透明度的使用

2.3.1 移除布局中不需要的背景

一些布局中的背景由于被该视图上所绘制的内容完全覆盖掉,因此这个背景实际上多余的,如果没有移除这个背景的话,将会产生过度绘制。我们可以使用以下方案来移除布局中不需要的背景:

1.移除Window默认的Background
2.移除控件中不需要的背景

2.3.1.1 移除Window默认的Background

通常,我们使用的theme都会包含了一个windowBackground,比如某个theme的如下:

 <item name="android:windowBackground">@color/background_material_light</item>

然后,我们一般会给布局一个背景,比如:

<android.support.constraint.ConstraintLayout
    ...
    android:background="@mipmap/bg">

这就导致了整个页面都会多了一次绘制。

那么其解决办法也很简单,把windowBackground移除掉就可以了,有以下两种方法来解决,随便使用其中一种即可:

1.在theme中设置

<style name="AppTheme" parent="主题">
    <item name="android:windowBackground">@null</item>
</style>

2.在ActivityonCreate()方法中添加:

getWindow().setBackgroundDrawable(null);

直接来看下优化前后的对比图:

优化前,由于需要绘制windowBackground以及布局的background,即有1次过度绘制,因此整个界面是蓝色的,同时hello world文字部分再进行了一次绘制,所以变绿了。

优化后,由于不需要绘制windowBackground,仅仅只需要绘制布局的background,因此整个界面显示的是原本的真彩色。文字部分再进行一次绘制,也只是蓝色而已。

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

下面先来看个例子,根布局LinearLayout设置了一个背景,然后它的子控件3个TextView中有两个设置了同样的背景,布局如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:background="#ffffffff"
              android:orientation="vertical">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#ffffffff"
        android:text="test0"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="test1"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#ffffffff"
        android:text="test2"/>
</LinearLayout>

其显示结果如下:

可以看到,2个使用了跟父布局同样背景的TextView会导致了一次过度绘制。

那么,我们平时只需要遵循以下两个原则就可以减少次过度绘制:

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

2.3.2 将layout层级扁平化

往往我们在写界面的时候都会使用基本布局来实现,这可能会出现一些性能问题。比如:使用嵌套的LinearLayout可能会导致布局的层次结构变得过深。另外,如果在LinearLayout中使用了layout_weight的话,那么他的每一个子 view都需要测量两次。特别是用在 ListViewGridView 时,他们会被反复测量。

布局嵌套过多的话会导致过度绘制,从而降低性能,因此我们需要将布局的层次结构尽量扁平化。

2.3.2.1 使用Layout Inspector去查看layout的层次结构

之前的Android SDK工具包含了一个名为Hierarchy Viewer的工具,可以在应用运行时分析布局。但是在Android Studio 3.1之后,Hierarchy Viewer就给移除掉了。并且Android的团队表示不再开发Hierarchy Viewer。所以这里就不介绍Hierarchy Viewer

这里使用Android推荐的Layout Inspector来查看layout的层次结构。

在Android Studio中点击Tools > Android > Layout Inspector。然后在出现的 Choose Process 对话框中,选择想要检查的应用进程即可。

Layout Inspector会自动捕获快照,然后会显示以下内容:

  • View Tree:视图在布局中的层次结构。

  • Screenshot:每个视图可视边界的设备屏幕截图。

  • Properties Table:选定视图的布局属性。

通过左侧View Tree即可看到布局中的层次结构。

偷偷提一句,Layout Inspector也可以用来分析别人APP的布局。

2.3.2.2 使用嵌套少的布局

假如要实现以下布局:

我们可以使用LinearLayoutRelativeLayout来完成。但是LinearLayout相比于RelativeLayout,就多了一层,所以RelativeLayout明显是一个更优的选择。如下图所示:

所以,合理选择不同的布局能够减少嵌套。

2.3.2.3 使用merge标签减少嵌套

通过<include>标签能够复用布局。

比如,我们要复用如下的一个布局,一个垂直的线性布局包含一个ImageViewTextView,其布局文件layout_include.xml如下:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <ImageView
        ...
        />
    <TextView
        ...
        />
</LinearLayout>

然后我们就可以通过<include>来复用这个布局了,其布局文件activity_include.xml如下:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#fff"
    android:orientation="vertical">
    <include layout="@layout/layout_include"/>
    <include layout="@layout/layout_include"/>
    <include layout="@layout/layout_include"/>
</LinearLayout>

但是上面这个例子会有个问题:其父布局是垂直的线性布局,include进来的也是垂直的线性布局,这就会造成了布局嵌套,而且这种嵌套是没必要的,那么就可以使用<merge>标签来减少这种嵌套。将layout_include.xml改成以下即可:

<merge
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <ImageView
        ...
        />
    <TextView
        ...
        />
</merge>

我们可以用Layout Inspector来看下使用<merge>标签优化前后的布局层次结构:

2.3.2.4 使用lint来优化布局的层次结构

lint是一个静态代码分析工具,可以用来协助优化布局的性能。要使用lint,点击Analyze> Inspect Code即可,如下图所示:

布局性能方面的信息位于Android> Lint> Performance下,我们可以点开它来看下一些优化建议。

下面是lint的一些优化技巧:

  1. 使用复合图片
    如果一个线性布局中包含一个 ImageView 和一个 TextView,可以使用复合图片来替换掉

  2. 合并根节点
    如果一个FrameLayout 是整个布局的根节点,并且也没有提供背景、留白等等,那么可以使用<merge>标签来替换掉,因为DecorView本身就是一个FrameLayout

  3. 移除布局中无用的叶子
    布局是一个树形的结构,如果一个布局没有子 View 或者背景,那么可以把它移除掉(这布局本身就不可见了)。

  4. 移除无用的父布局
    如果一个布局没有兄弟,也不是ScrollView 或者根 View,并且也没有背景,那么可以把这个父布局移除掉,然后把它的子view移到它的父布局下。

  5. 避免过深的层次结构
    过多的布局嵌套不利于性能,可以使用更扁平化的布局,如RelativeLayoutGridLayoutConstraintLayout等布局来提高性能。布局默认的最大深度为10。

lint的功能其实很强大,可以用来检测优化各个方面,平时我们遇到lint的一些警告,能修复优化的话就尽量去完善掉。

2.3.3 减少透明度的使用

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

2.3.4 减少自定义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.length; i++) {
        canvas.save();
        if (i < imgs.length - 1) {
            //前面的n-1张图,只裁剪一部分
            canvas.clipRect(i * 100, 0, (i + 1) * 100, imgs[i].getHeight());
        } else if (i == imgs.length - 1) {
            //最后一张,完整的
            canvas.clipRect(i * 100, 0, i * 100 + imgs[i].getWidth(), imgs[i].getHeight());
        }
        canvas.drawBitmap(imgs[i], i * 100, 0, mPaint);
        canvas.restore();
    }
}

优化后的效果图如下:

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

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

3.一些布局优化技巧

除了避免过度绘制之外,还有一些其他的优化技巧能够帮我们提升性能。这里简单介绍一下一些比较常用的技巧。

3.1 使用性能更优的布局

  1. 在无嵌套布局的情况下,FrameLayoutLinearLayout的性能比RelativeLayout更好。因为RelativeLayout会测量每个子节点两次。

  2. ConstraintLayout的性能比RelativeLayout更好,推荐使用ConstraintLayout。后面会介绍ConstraintLayout的使用。

3.2 使用include标签提高布局的复用性

使用<include>标签提取布局的公用部分,能够提高布局的复用性。具体例子这里就不写了,可以回头看看<merge>标签那一小节的例子。

3.3 使用ViewStub标签延迟加载

在项目中,有些复杂的布局很少使用到,比如进度指示器等等。那么我们可以通过<ViewStub>标签来实现在需要时才加载布局。使用<ViewStub>能够减少内存的使用并且加快渲染速度。

ViewStub是一个轻量级的视图,它没有尺寸,也不会绘制任何内容和参与布局。下面是一个ViewStub的例子:

<ViewStub
    android:id="@+id/stub_import"
    android:inflatedId="@+id/panel_import"
    android:layout="@layout/progress_overlay"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom" />

这里的panel_import就是具体要加载的布局ID。

通过以下代码即可在需要时加载布局:

findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
或者
View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();

一旦布局加载后,ViewStub就不再是原来布局的一部分了,它会被新加载进来的布局替换掉。需要注意的是,ViewStub不支持<merge>标签。

3.4 onDraw()中不要创建新的局部变量以及不要做耗时操作

  1. onDraw()中不要创建新的局部变量,因为onDraw()方法可能会被频繁调用,大量的临时对象会导致内存抖动,会造成频繁的GC,从而使UI线程被频繁阻塞,导致画面卡顿。

  2. Android要求每帧的绘制时间不超过16ms,在onDraw()进化耗时操作的话,轻则掉帧,严重的话会造成卡顿。

3.5 使用GPU呈现模式分析工具来分析渲染速度

点击开发者模式->监控->GPU呈现模式,然后选择 在屏幕上显示为条形图 即可以看到一个图表。

如下图所示:

上图中,主要包含了以下信息:

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

再来看下每个竖条的颜色代表什么意思:
注意:这是在Android6.0以上才有的颜色,6.0以下只有3、4种,所以建议使用6.0以上的设备来查看。

如果存在一大段的竖条都超过了绿线,则我们可以去分析是哪个阶段的时间花费比较多,然后针对性的去优化。

4. 使用ConstraintLayout

ConstraintLayout是Android新推出的一个布局,其性能更好,连官方的hello world都用ConstraintLayout来写了。所以极力推荐使用ConstraintLayout来编写布局。

ConstraintLayout,可以翻译为约束布局,在2016年Google I/O 大会上发布。我们知道,当布局嵌套过多时会出现一些性能问题。之前我们可以去通过RelativeLayout或者GridLayout来减少这种布局嵌套的问题。现在,我们可以改用ConstraintLayout来减少布局的层级结构。ConstraintLayout相比RelativeLayout,其性能更好,也更容易使用,结合Android Studio的布局编辑器可以实现拖拽控件来编写布局等等。

— — — END — — —

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