Android系统的卡顿,不流畅是一个被诟病多年,让很多用户觉得体验不如iOS的问题。但是经过Google多年对Android系统的迭代,实际上在系统层面上Android已经在UI渲染性能上做出了很多改进和优化,对于应用开发者而言,很大程度上,写不出流畅的页面和动画已经不再是系统的锅,而是应该努力去学习和理解Android系统的特性,发现在渲染过程中的瓶颈,做出合理的优化。
回顾Android的历史版本,我们可以一览Google Android团队在各版本上面做出的具体改进。
Android2.x时代,UI渲染采用CPU软渲染,基本上滑动列表只能维持在30fps左右,那个时代硬件性能和系统架构确实都不够好。
- 2011年,Android 3.0 Honeycomb发布,Android开始支持GPU硬件加速渲染,采用OpenGL渲染UI,第一款Android 3.0的设备是MOTO ZOOM,一款分辨率为1280800像素的平板电脑,第一次达到如此高的分辨率( 此前的Android手机最高分辨率为480854),如果说没有硬件加速,Android系统仅靠CPU很难流畅渲染这么高的分辨率。
* 2012年,Android4.1 JellyBean推出的“Project butter”(黄油计划)带来了vSync(垂直同步)和tripple buffer(三重缓冲)技术。vSync定时中断让Android系统渲染更加有节奏,tripple buffer让系统在偶尔丢帧的情况下通过多生成一个buffer解决了之前在丢帧时因buffer不足而导致没法按时渲染下一帧的问题。此外还优化了触摸事件,通过CPU input boost和预测触摸行为减少触摸延时,让系统响应更加灵敏。Android4.1是我认为Android UI渲染架构革新史上里程碑式的一个版本,Cortex双核 A9 CPU+PowerVR SGX540 GPU的Galaxy Nexus渲染720p的屏幕,刷上JellyBean系统之后对比4.0 ICS是巨大的提升,基本能流畅运行在60fps。
* 2013年,Android4.3 JellyBean发布,在图形渲染架构上更进一步优化,通过对GPU绘制指令的Reorder&Merge,减少了GPU在绘制时上下文的切换和绘制指令执行的数量,同样的内容能以更小的消耗去渲染,提高了渲染效率从而提高了渲染性能。
- 2014年,Android5.0 Lollipop发布,带来了全新的Material Design设计风格和分担主线程负载的RenderThread,在主线程更新完DisplayList之后,将draw commands全部都同步到RenderThread,然后由RenderThread继续去与OpenGL Render(GPU)打交道完成最终的绘制,而主线程可以在同步完draw commands之后开始处理接下来的input event或进入Idle状态,很明显这减少了主线程的负担,也能让主线程更快抽身出来响应用户的操作减少延迟。另外在较新版本的RecyclerView库中,通过一个叫GapWorker的类在Android 5.0及以上利用主线程在同步完draw commands之后等待下一帧绘制的这一点空闲时间,对将要显示的ViewHolder做了预加载操作,从而让列表在接下来的滑动中更加流畅。
之后这些年的Android6.x-8.x版本对于性能的改进主要集中在ART Runtime执行效率和速度的改进,以及更加严格的后台运行限制。代码执行更快,后台应用活动受到限制从而减少了与前台应用抢资源的可能,一定程度上也是提高了UI渲染性能。不过也有一些手机厂商的定制系统(ROM),修改系统框架,加了太多定制功能,带来了一些“负优化”,相同甚至更好的硬件性能,运行流畅度比不上相同版本的原生系统。总而言之从5.0开始,Android系统的UI渲染架构确实已经足够好,而且硬件性能还在以每年20-30%的速度进步,所以在如今的软硬件环境下,我们完全可以通过合理优化写出滑动流畅的界面和过渡优雅的动画。
我们知道UI渲染大致分为 Measure->Layout->Draw三个阶段,这也是我们编写自定义View必须自己实现的三个方法。
根据多年对Android系统的了解和开发经验的总结,列出了如下表格
在Measure/Layout阶段,我们可能更多关注ViewGroup,即RelativeLayout,ConstraintLayout,LinearLayout,FrameLayout等这些布局类。整体的一个原则就是,尽量减少子view的数量和layout的嵌套。
因为过深的View层级会导致测量阶段耗时较多,复杂的依赖和约束关系可能导致多次测量。我们可以通过很多系统提供的工具来查看页面的View层级,仔细审视,看是否有可优化的地方。
Hierarchy Viewer是Android SDK里面提供的一个可以查看调试应用Activity页面View层级的工具,并且可以手动触发页面重新渲染,观察每个view节点的各个阶段的耗时情况。
具体的使用教程可以参看官方文档:developer.android.com/studio/prof…
不过这个工具目前已经被官方作废了,Android Studio里面推出了新的LayoutInspector来代替他,不过目前LayoutInspector只能查看View层级无法查看节点渲染耗时。
LayoutInspector是集成在Android Studio里面的工具,可以详细查看调试应用的View层级,以及view节点的各种属性值。
另外还有开发者选项里面的“显示布局边界”,也可以让我们比较方便的查看当前界面的View节点组成和分布情况。与Hierarchy Viewer和LayoutInspector不同的是,
显示布局边界可以查看任何应用界面的View节点大小和位置。不过他查看到的信息有限,只能根据描线看到View节点的边界,位置等信息。 也许是出于安全考虑,无法查看到更多信息,不过我们可以从黑盒角度审视其他应用界面的布局学习别人的做法和思路。
1.通过合并多个要显示的数据到一个View来实现减少View个数
查看界面View层级的目的是为在不影响功能实现的情况下尽可能减少View数量和优化View层级,我们可以通过合并多个要显示的数据到一个View上来实现减少View的目标, 如下图展示了几种优化方案
2.采用ConstraintLayout减少布局嵌套
传统方式,对于如下布局我们可能会采用RelativeLayout和LinearLayout嵌套的方式完成布局,虽然RelativeLayout可以定义view之间的相对关系和约束关系,
但是他无法完成百分比布局,按照权重weight布局,宽高比等布局的场景,所以我们为了完成复杂界面的布局,往往需要大量的嵌套不同的layout来组合完成。
所以2017年Google在I/O大会上推出了ConstraintLayout来解决此问题,他不仅拥有空前强大的布局约束属性,更重要的是性能比RelativeLayout更好,
他足够丰富的布局功能以致于不需要嵌套就能完成非常复杂的界面布局,所以Google推出ConstraintLayout的目的就是为了取代RelativeLayout
来优化UI布局的性能。而且ConstraintLayout是一个不断开发迭代的layout库,不断优化的性能和扩展的功能让他越来越强大,学会使用ConstraintLayout 是每一个位Android开发者都必须要学会的高级技能。
关于学习ConstraintLayout,这里推荐一些文章供大家学习。相信大家学会ConstraintLayout之后, 就不会再想用RelativeLayout了,而且也尽量不要再用RelativeLayout,因为即使实现相同功能其性能相对ConstraintLayout也不如。
如下界面是采用ConstraintLayout布局的相同界面,可以看到采用ConstraintLayout之后,View的数量和层级都大为减少,这可以显著明显减少View在 测量阶段的耗时。
推荐选择Layout顺序:优先FrameLayout(性能最好)>LinearLayout(性能次之)>ConstraintLayout(功能最强大,复杂布局用它)。
3.LinearLayout关闭baselineAligned
LinearLayout有一个属性叫android:baselineAligned,默认值为true,即默认开启baseline基线对齐,是LinearLayout内有多个横向排布的TextView需要优化文字对齐效果的时候一个开关。
如下图所示我们可以大概了解一下baseline的含义。
如果我们的LinearLayout内的多个TextView不需要基线对齐,或者说并不包含TextView而是一些其他的View,那么我们可以关闭此功能来减少测量的耗时,避免开启后因对齐基线而多测量一次。
下图是LinearLayout measure时的部分代码,很明显可以看到因为对齐基线而多了一次测量。
4.ViewStub延迟初始化
ViewStub是一个不可见,零尺寸的View,可在使用时可以按需初始化布局。当ViewStub被设置为visible或者调用inflate方法时,指定的layout会被加载填充, 被加载的layout将代替ViewStub添加到ViewStub的父布局中。
我们可以通过阅读ViewStub的源码看到他在未inflate时几乎无开销,所以对于部分一开始不使用的layout,可以采用此种方式加载,提高初次inflate和渲染主界面的速度。
5.include布局时善用merge标签,减少嵌套
当我们有一些layout可以复用时,我们往往可以把它单独写到一个layout文件里面,然后需要的时候再include进主layout里面,layout文件往往需要一个根布局来包装所有的子View(ViewGroup),
而这个根布局被include之后往往会因为上层是一个同样的ViewGroup变得多余,这里我们可以用Merge标签作为根布局来包含所有子view而去除原有的ViewGroup,减少一层嵌套。
介绍完了mesure/layout阶段的优化技巧,我们再来看看在draw阶段我们可以做哪些优化。 在进行优化之前首先给大家介绍一个非常重要的查看界面渲染流畅度的工具,GPU呈现模式分析,通过他我们可以很直观的观察界面渲染的帧率和流畅度
关于每条竖线的分段颜色的含义,如下是一个详细介绍,他们代表了每一帧渲染过程的各个阶段,分段长度则代表这一阶段的耗时
5.减少Overdraw过度绘制
过度绘制指的是在同一块屏幕空间内发生的重叠绘制次数,这是不可避免会存在的问题,我们能做的就是尽量减少这种现象。
很幸运开发者选项里面的“调试GPU过度绘制”提供了相应的工具让我们可以很直观的观察到这种现象
发生过度绘制我们能优化的主要是避免绘制看不见的元素,这里面最容易犯错的地方就是绘制了看不见的View背景。
下图显示了我们通过去掉多个ViewGroup的背景色,然后仅在窗口背景上设置需要的背景颜色,减少了Overdraw又不影响功能实现。
下图是去掉多余背景之后的效果,结果非常明显,大幅减少了overdraw现象,这样在draw的阶段可以有更好的性能表现。
6.Canvas.clipRect/quickReject
Canvas.clipRect()可以定义绘制的边界,边界以外的部分不会进行绘制。
Canvas.quickReject()可以用来测试指定区域是否在裁剪范围之外,如果要绘制的元素位于裁剪范围之外,就可以直接跳过绘制步骤。
下图演示了一个包含多个层叠View的自定义ViewGroup,当发生重叠时不可避免要产生很严重的过度绘制情况,但实际上底层被遮盖的View的某一部分是不可见的, 我们可以通过在ViewGroup drawChilds的时候,通过计算显示和不显示的区域来做合理的clip,避免绘制看不见的部分。
实际上如果你看过DrawerLayout的源码,他里面的实现也有类似这样的优化,在Drawer打开的时候底下View被遮盖的部分区域被clip不绘制。
通过下图我们可以明显看出来,Drawer并没有比右边没遮盖的区域绘制更多层,因为Drawer底下被遮盖的区域被clip做了优化。
7.占位背景图优化
在显示图片的时候,我们有些时候可能需要给图片加边框和阴影或者设置默认占位图片做修饰,如果当图片加载完了之后我们仍然需要显示占位图来修饰原图, 那么可能在被目标图遮盖的那部分区域就多绘制了一层占位图的背景,导致即使不显示也要被绘制。
所以这里我们提出一种优化方法,根据不同的状态显示不同的占位修饰图。当未加载完成是显示带背景的占位图,当加载完成后我们显示透明背景的占位图 这样被目标图遮盖的区域就不会多绘制一层无用的背景了。
优化完了之后结果如下,很明显,使用透明背景的边框图少了一次绘制。
在这里总结一下:避免Overdraw的核心原则始终只有一个,就是避免绘制看不见的元素
8.Alpha blending透明度合成优化
如果直接对每个View分别做alpha合层会导致丢掉他们之前的层叠效果,导致看见被覆盖的底层的View,显然这不是我们想要的结果
我们想要的结果是如下图所示,Alpha合成之后仍然能保留堆叠信息。实际上系统已经为Alpha合层做出了合理处理,在帧缓冲(FrameBuffer)绘制之前,
会创建离屏缓冲区(off-screen buffer/Canvas Layer)进行绘制,然后以一个整体进行透明度合层,结果再被复制到帧缓冲。也就是说在绘制带透明度的View时, 我们需要双倍的开销。
为了避免因为透明度合层导致绘制时双倍的开销,有些需要alpha合层的地方我们可以做一些优化,比如TextView需要设置alpha值,如果没有背景,我们可以直接修改文字颜色值,
在原颜色值的基础上加入透明度混合计算之后直接修改文字颜色值,这样避免alpha合层时创建离屏缓冲从而减少开销提高性能。
对于自定义View,我们可以通过重写onSetAlpha方法返回true来向上层框架表明自己有能力处理alpha值的变化,通过将alpha值设置到paint上, 在draw的时候直接处理了alpha相关信息。
8.重写hasOverlappingRendering方法
hasOverlappingRendering顾名思义就是是否存在重叠渲染的意思。hasOverlappingRendering方法是打算让继承自View的子类覆盖重写的方法,
当View的alpha<1时他是一个优化点,默认返回true会在渲染时启用离屏缓冲导致双倍开销,而如果View没有重叠绘制的话,那么可以返回false,
这样View在alpha<1的情况下绘制时就不会启用离屏缓冲,一定程度上优化了渲染性能。
下面是ImageView的源码,可以看到当判断没有background的时候,重写hasOverlappingRendering返回false,优化了alpha合成性能。
9.Use Hardware Layer
当给一个View.setLayerType(View.LAYER_TYPE_HARDWARE,null)后,就定义了此View采用Hardware Layer(背后采用一个硬件相关纹理)来加速渲染。
Hardware Layer对于ViewGroup以及其所包含的子Views一起做Alpha Blending,ColorFilter非常有用。Hardware Layer可以缓存整个复杂的View树到一个纹理上, 减少绘制命令,当需要对整个View树做动画的时候,只需要渲染一次(层)。
下面这段代码显示了我们执行一个旋转动画,开启或不开启HW Layer的对比效果
实际运行效果如下,可以看到ViewGroup初始状态是包含4个堆叠的View,自己还有背景色,过渡绘制很严重有很多层,
未开启HWLayer做动画的时候,依然需要渲染很多层,导致帧率不高经常顶到16ms的基线在跑,而开启HW Layer之后,
优化效果立竿见影,ViewGroup及其子View都被合层为一个HWLayer上渲染,只需要绘制一层,运行动画的帧率明显流畅了不少。
##最后总结一下Draw阶段的优化原则:
- 尽量减少不必要的绘制。可以通过减少overdraw,Avoid Alpha Blending,clip Canvas,Use Hardware Layer等手段来完成。
- 避免在调用频繁的路径(eg:onDraw,onBindViewHolder)创建对象,格式化数据和做大量的计算,善用缓存和局部刷新机制。
UI性能优化是一个永无止境的工作,如果我们能把所有可优化的点都优化好,一点点积累优势,量变就会产生质变,最后一定达会到满意的效果。