课程名称:Top团队大牛带你玩转Android性能分析与优化
课程章节:App性能概览与平台化实践
主讲老师:随风绽放
课程内容
1.Android 绘制原理及工具选择
在计算机硬件中,通常 CPU 用来处理数据,GPU 用来渲染数据,Android 系统也不例外,绘制过程首先是 CPU 准备数据,通过 Driver 层把数据交给 GPU 渲染,其中 CPU 主要负责 Measure、Layout、Record、Execute 的数据计算工作,GPU 负责 Rasterization(栅格化)、渲染。
无论是 CPU 准备的数据,还是 GPU 要渲染的数据,都是一帧一帧的形式。我们看到的界面也是由一帧一帧的图像连续显示而来。对于人眼来说,每秒看到 60 帧是比较流畅的,即 FPS(Frame Per Second)为 60,1/60 = 0.0167,即每 16 ms进行一次准备、渲染工作。
常用的布局优化工具有以下几种:
-
- Systrace,内存优化工具中也用到了 Systrace,这里关注 Systrace 中的 Frames 页面,正常情况下圆点为绿色,当出现黄色或者红色的圆点时,表现出现了丢帧。
-
- Layout Inspector,是 AndroidStudio 自带工具,可以查看页面的视图层次结构。
-
- Choreographer,是 Android16 加入的一个工具类,通过 Choreographer.getInstance().postFrameCallback,可以实时获取 FPS。
Android 的布局加载时 IO 操作,比如 xml 文件要通过 xml 解析器才能加载到内存里。在加载 xml 文件的时候,还会用到反射,而反射本来性能就不高。
2.优雅获取界面布局耗时
在对布局优化前,我们需要找到一个指标,作为衡量布局优化效果的标准。常用有下面几种方式来获取页面加载的耗时。
- 常规方式,通过手动埋点的方式来确认页面加载的耗时,这种方式代码侵入性强,工作量大,不便于后期的维护。
- AOP 方式,通过对 Activity 的 setContentView()方法切片,获取加载每个页面的耗时,这种方式代码侵入性小。
通过上面的方式可以获取页面加载的耗时,为了更加精细化的优化,如何获取每个控件加载的耗时呢? 可以通过 LayoutInflater.Factory(考虑兼容性可以使用 LayoutInflaterCompat.setFactory2) 来实现,这种方式代码侵入性小。通过自定义 Factory,在 onCreateView() 方法的中实现对每个控件加载耗时的计算。使用时需要注意,这个设置应该在super.onCreate()之前,否则无效。
public interface Factory {
/**
* Hook you can supply that is called when inflating from a LayoutInflater.
* You can use this to customize the tag names available in your XML
* layout files.
*
* <p>
* Note that it is good practice to prefix these custom names with your
* package (i.e., com.coolcompany.apps) to avoid conflicts with system
* names.
*
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
public View onCreateView(String name, Context context, AttributeSet attrs);
}
3.异步 Inflate
前面介绍到布局文件加载的过程是一个 IO 过程,创建对象时还使用到反射,使用反射创建对象的速度比通过 new 关键字创建要慢 3 倍左右。所以布局优化的方向是尽量减少 IO 过程,减少通过反射来创建对象。
这里介绍一种侧面优化布局的方式即异步实现布局的加载。AsyncLayoutInflater 是一个异步初始化布局的 Helper 类。它的本质就是把对布局文件的 inflate 放入到了子线程里面,等到初始化成功后,在通过接口抛回到主线程。
4.布局加载优化
有没有一种方法从根本上来解决io操作慢、反射慢的问题呢?答案是直接用 Java 代码写布局文件,这样会引入新问题,不便于开发、可维护性差。用 xml 来写布局,虽然IO慢、反射慢,因为方便维护,可读性好,还方便写UI时进行预览。那有没有一个方案既能保留XML优点,还能解决其性能问题呢?X2C 就是这样一个方案。
X2C 的原理是在 APT 编译期将需要翻译的布局 xml 文件翻译生成对应的 java 文件,这样对于开发人员来说写布局还是写原来的 xml ,但对于程序来说,运行时加载的是对应的 java 文件。
不论是异步加载 AsyncLayoutInflater,还是 X2C 方案,都要考虑下面的问题:
-
- 控件的一部分属性在 xml 文件中使用是可以的,在 Java 代码中是不支持的,这部分属性虽然很少,但还是存在,使用的时候需要注意。
-
- 另外一个问题是失去了系统的兼容性,无法使用 AppCompat 等兼容类,无法做到控件的向下兼容。这时就要考虑对两种方案进行定制,来获得兼容性。
5.视图绘制优化实战
前面对布局优化的方案都是针对布局加载进行优化的,这里介绍一下在视图绘制时的优化方案。Android 中的视图绘制要经历三个过程:
- 测量,在测量阶段确定视图的大小;
- 布局,在布局阶段确定视图的位置;
- 绘制,在绘制阶段完成视图的绘制。
三个过程的实现都会涉及 View 树自顶向下的遍历,有时甚至还会触发多次,所以每个阶段都会出现耗时的情况,都是我们去优化的方向。布局优化在绘制阶段的准则是尽量减少 View 树的层级,页面布局要做到宽而浅、避免窄而深。
ConstraintLayout 是在 Android Studio 2.2 中主要的新增功能之一,它可以有效地解决布局嵌套过多的问题,实现几乎完全扁平化布局,构建复杂布局的性能更高,它有点类似于RelativeLayout,但远比RelativeLayout要更强大。除了使用 ConstraintLayout,还可以考虑以下的优化方式:
- 不嵌套使用 RelativeLayout;
- 不在嵌套的 LinearLayout 中使用 weight 属性;
- 使用 merge 标签,可以减少一个 view 层级,但是只能用在根 View 中;
过度绘制就是在同一个区域中叠加了多个控件,重复的叠加就是过度绘制,很可能会造成刷新率下降,造成卡顿的现象。对于过度绘制的测试主要通过人工进行测试,也是发现应用过渡绘制的首选途径,通过打开开发者选项中的 显示GPU过度绘制。过渡绘制可以通过颜色标识,按照从好到差依次是:蓝、绿、淡红、红。
- 蓝色1x过度绘制
- 绿色2x过度绘制
- 淡红色3x过度绘制
- 红色超过4x过度绘制
避免过度绘制的方法:
- 去掉多余背景色,减少复杂shape使用;
- 避免层级叠加;
- 自定义 View 使用 clipRect 屏蔽被遮盖View绘制;
布局优化的其它技巧:
- 使用 ViewStub 标签来加载一些不常用的布局,ViewStub 是一个高效的占位符可以实现延迟初始化。
- 在 onDraw() 方法中避免创建大对象和其它耗时操作。
- TextView 的优化,TextView中很多操作都很繁重。比如setText操作,须要设置SpanWatcher,或者要重现创建一个 SpannableString,还要依据情况又一次创建TextLayout,这些操作加起来之后令一次 setText 操作很耗时。可以通过自定义 view 的方式来优化,对于可编辑的文本,使用 DynamicLayout 来实现,对于静态文本,推荐使用StaticLayout。
课程收获
课程从布局加载、布局绘制的角度介绍了布局优化的方案,涵盖了布局优化的所有手段。既提供了获取优化指标的方式,又提供了优化的手段,非常实用。