1 目录
image2 前言
本系列的前面几篇文章我们介绍了布局加载的原理及优化,布局加载完成后(生成VIew对象)就要进行视图绘制,我们知道,android要求每帧的绘制时间不超过16ms,不然就会导致丢帧及应用卡顿。所以本文将会介绍一些布局绘制优化技巧
3 如何监控应用渲染速度
点击设置—>开发人员选项—>监控—>GPU呈现模式分析,然后选择 在屏幕上显示为条形图 即可以看到一个图表,如下图所示
image1.沿水平轴的每个竖条都代表一个帧,每个竖条的高度表示渲染该帧所花的时间(单位:毫秒)
2.水平绿线表示 16 毫秒。 要实现每秒 60 帧,代表每个帧的竖条需要保持在此线以下。 当竖条超出此线时,可能会使动画出现暂停
再来看下每个竖条的颜色代表什么意思:
image4 分析从哪些方向进行绘制优化
从GPU呈现模式分析可以看出来,我们能够进行优化的点主要就是测量、布局、绘制、动画和输入处理
- 测量、布局、绘制过程都会存在自顶而下遍历过程,所以如果布局的层级过多,这会占用额外的CPU资源
- 当屏幕上的某个像素在同一帧的时间内被绘制了多次(Overdraw),这会浪费大量的CPU以及GPU资源
- 在绘制过程,也就是
onDraw()
方法内,我们应该尽量避免局部对象的创建,因为onDraw()
方法在绘制过程中会多次调用,大量的局部变量可能会造成内存抖动 - 合理使用动画,这个本章不做讨论,有兴趣的可以自己了解动画的相关知识
- 不应该在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
为准
我们可以通过下面两个方法来解决这个问题
- 在xml中设置
<item name="android:windowBackground">@null</item>
通过代码设置
getWindow().setBackgroundDrawable(null);
6.2 移除控件中不需要的背景
例子:
- 列表页(
RecyclerView
) 与 其内子控件(Item
)的背景相同,故可移除子控件(Item
)布局中的背景 - 对于1个
ViewPager
+多个Fragment
组成的首页界面,若每个Fragment
都设有背景色,即 ViewPager 则无必要设置,可移除
所以对于控件背景颜色的设置基本可以归纳为以下两个原则:
- 对于子控件,如果其背景颜色跟父布局一致,那么就不用再给子控件添加背景了
- 如果子控件背景五颜六色,且能够完全覆盖父布局,那么父布局就可以不用添加背景了
6.3 减少透明度的使用
对于不透明的view
,只需要渲染一次即可把它显示出来。但是如果这个view
设置了alpha
值,则至少需要渲染两次。这是因为使用了alpha
的view
需要先知道混合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
标签
- 自定义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>
- 有时候我们会通 过
include
标签来提高布局的复用性,如果layout_include_xx.xml
的布局和其父布局使用的是同一个布局类型,如线性布局等。这时候就可以在layout_include_xx.xml
中使用merge
标签来减少布局层级
6.6 使用ViewStub标签延迟加载
ViewStub
是一个不可见的View
类,用于在运行时按需懒加载资源,只有在代码中调用了viewStub.inflate()
或者viewStub.setVisible(View.visible)
方法时才内容才变得可见。这里需要注意的一点是,当ViewStub
被inflate
到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
标签使用注意点:
-
ViewStub
标签不支持merge
标签。因此这有可能导致加载出来的布局存在着多余的嵌套结构,具体如何去取舍就要根据各自的实际情况来决定了 -
ViewStub
的inflate
只能被调用一次,第二次调用会抛出异常 -
虽然
ViewStub
是不占用任何空间的,但是每个布局都必须要指定layout_width
和layout_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和反射为突破口,也可以通过异步加载从侧面环境这个问题。而布局绘制优化致力于解决过度绘制问题。本系列文章(布局优化)到此就结束了,希望对你有所帮助
网友评论