文章目录
- 1 渲染性能
- 2 渲染管线
- 3 Android UI和GPU
- 4 过度绘制
- 4.1 查看过度绘制
- 4.2 查看绘制渲染性能
- 4.3 过度绘制的解决
- 4.3.1 多余背景
- 4.3.2 View重叠
- 4.3.3 9-patches
- 5 渲染管道中的CPU部分
- 6 减少布局层级嵌套
- 6.1 ConstraintLayout
- 6.2 布局标签
- 7 硬件加速
- 7.1 软件绘制
- 7.2 硬件加速绘制
- 7.3 硬件加速的限制
1 渲染性能
渲染功能是应用程序最普遍的功能,一方面,设计师要求为用户展现可用性最高的超然体验,另一方面,那些华丽的效果和动画并不是在所有的设备上都能流畅运行。
那什么是渲染性能?
应用程序在其整个生命周期中,始终保持60FPS(Frame per second)的帧速率,即屏幕需要在每一秒内刷新60次,也就是每16.6667毫秒刷新一次,才能确保应用程序有一个流畅的体验。
但如果你超过了16ms的时间刷新,Android系统尝试在屏幕上绘制新的一帧,但是这一帧还没准备好,所以画面就不会刷新,这样就会出现我们所称的丢帧的情况,也就是会出现卡顿。
2 渲染管线
Android系统的渲染管线分为两个关键组件,CPU
和 GPU
。CPU
负责包括 Measre
、Layout
、Record
、Execuute
的计算操作,GPU
负责 Resterization
栅格化。两者共同工作在屏幕上绘制图片,每个组件都有自身定义的特定流程,你必须遵守这些特定的操作规则才能达到效果。
CPU最常见的性能问题是,不必要的布局和失效,这些内容必须在视图层次结构中进行测量、清除并重新创建。引发这种情况的通常有两个原因:
- 重建
DisplayList
的次数太多 - 花费太多时间作废视图层次,并进行不必要的重绘
这两个原因在更新 DisplayList
或者其他缓存GPU资源时,导致CPU工作过度。
GPU出现的问题是,在像素着色过程中,通过其他工具进行后期着色时,浪费了GPU处理时间。也就是所谓的透支。
3 Android UI和GPU
想要开发一款优秀流畅的应用,我们必须了解底层是如何运行的,如果你不知道硬件如何运行,你就无法熟练使用它们。
那么就会有一个问题:Activity是如何绘制到屏幕上的?我们xml布局上的控件布局是如何转化成用户能看懂的图像的?
实际上,这是由Resterization格栅化操作来完成的。格栅化将诸如字符串、按钮、路径或者形状的一些高级对象拆分到不同的像素上在屏幕上进行显示。格栅化是一个非常费时的操作,
手机里有一块特殊硬件,目的就是加快格栅化的操作,图像处理单元,也就是GPU。
现在GPU使用一些指定的基础指令集,主要是 Polygons
多边形和 Textures
纹理,也就是图片。CPU负责把UI组件计算成 Polygons
和 Textures
,CPU在屏幕上绘制图像前,会向GPU输入这些指令,这一过程通常使用的API就是Android的 OpenGL ES
。
在屏幕上绘制UI对象时,无论是按钮、路径或者复选框,都需要在CPU中首先转换为多边形或者纹理,然后再传递给GPU进行格栅化。
在控件转换为多边形或纹理,或者CPU将数据传递给GPU进行格栅化,这都是耗时的操作。所以我们应该减少对象转换的次数,以及上传数据的操作。
幸亏,OpenGL ES允许将数据传递到GPU后,可以对数据进行保存,但下一次遇到相同的控件绘制时,只需要在GPU存储器里引用它,告诉OpenGL怎么绘制。
渲染性能的优化就是尽可能的上传数据到GPU,然后尽可能长地在不修改的条件下保存数据,因为每次上传数据到GPU时都会浪费宝贵的处理时间。
Android系统在降低、重新利用GPU资源方面做了很多工作,这方面完全不用担心。举例说,任何你的主题所提供的资源,例如Bitmap
、Drawables
等都是一起打包到统一的纹理当中,然后使用网格工具上传到GPU,例如 9-pathces
等,这样每次需要绘制这些资源时,就不用做任何转换,它们已经存储在GPU中了,大大加快这些视图类型的显示。
Android已经帮我们处理了很多渲染性能方面的问题,但是,还有一个GPU的问题,就是过度绘制。
4 过度绘制
4.1 查看过度绘制
过度绘制描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的UI结构里面,如果不可见的UI也在做绘制的操作,会导致某些像素区域被绘制了多次。过度绘制造成的原因有很多,比如背景颜色多余绘制,View的层叠等。
而查看我们的应用是否有过度绘制,可以在手机的开发者选项开启显示GPU过度绘制调试开关。分别有蓝色、淡绿、淡红、深红四种不同程度的过度绘制情况,我们的目标是尽量减少红色的过度绘制,看到更多的蓝色区域。
4.2 查看绘制渲染性能
除了查看过度绘制的调试工具,Android也提供了 Profile GPU Rendering
工具方便我们查看渲染性能情况,同样可以在开发者选项开启GPU呈现模式分析。
开启之后就可以在手机画面上看到丰富的GPU绘制图形信息。
随着界面的刷新,界面上会滚动显示垂直的柱状图来表示每帧画面所需要渲染的时间,柱状图越高表示花费的渲染时间越长。
中间有一根绿色的横线代表16ms,我们需要确保每一帧花费的总时间都低于这条横线,这样才能够避免出现卡顿的问题。
每一条柱状线都包含三部分,蓝色代表测量绘制 DisplayList
的时间,红色代表OpenGL渲染 DisplayList
所需要的时间,黄色代表CPU等待GPU处理的时间。
仅仅显示三大步骤的时间耗费情况还是不太能够清晰帮助我们定位具体的程序代码问题,所以从Android M开始,GPU Profiling工具把渲染操作拆解成如下8个详细步骤进行显示:
旧版本中的 Process
、Execute
、Update
还是继续得到了保留,对应关系如下:
接下来看下其他五个步骤分别代表了什么含义:
- Sync & Upload:通常表示的是准备当前界面上有待绘制的图片所耗费的时间,为了减少该段区域的执行时间,我们可以减少屏幕上的图片数量或者是缩小图片本身大小
- Measure & Layout:表示布局的
onMeasure()
和onLayout()
所花费的时间,一旦时间过长,就需要仔细检查自己的布局是不是存在严重的性能问题 - Animation:表示计算执行动画所需要花费的时间,包含的动画有
ObjectAnimator
、ViewPropertyAnimator
、Transition
等等。一旦这里的执行时间过长,就需要检查是不是使用了非官方的动画工具或者是检查动画执行的过程中是不是触发了读写操作等等 - Input Handling:表示系统处理输入事件所耗费的时间,粗略等于事件处理方法所执行的事件。一旦执行时间过长,意味着在处理用户的输入事件的地方执行了复杂的操作
- Misc/VSync Delay:如果稍加注意,我们可以在开发应用的Log日志里面看到这样一行提示:
I/Choreographer(691):Skipped XXX frames! The application my be doing too much work on its main thread
。这意味着我们在主线程执行了太多任务,导致UI渲染跟不上VSync
的信号而出现掉帧的情况
上面的分类比较详细,但大体上还是可以区分为三个部分:
- 绿色:主要处理一些 CPU 的计算,比如纹理计算、动画、测量摆放等。一般我们会主要关注该指标,尽可能的降低绿色柱形条的高度和数量
- 蓝色:处理绘制
- 黄色:GPU 渲染
4.3 过度绘制的解决
4.3.1 多余背景
视图层级结构中最顶层的视图为 DecoreView
,在主题(theme)中为Activity设置的背景,就是通过它来显示。这个默认背景通常会被所编写的布局文件中的背景所遮盖,这意味着会给GPU带来额外的工作,减慢渲染速度,影响帧速率。
比如界面的背景颜色是白色的,我们就可以去除主题默认的设置背景。
移除背景图片方法:
<resources>
<style name="Theme.NoBackground" parent="android:Theme">
<item name="android:windowBackground">@null</item>
</style>
</resources>
一个更好的实践方式是,将Activity布局的背景移到窗口的 DecoreView
中:
<resource>
<style name="Theme.NoBackground" parent="android:Theme">
<item name="android:windowBackground">@drawable/background</item>
</style>
</resources>
代码移除方式:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if (hasFocus)
getWindow().setBackgroundDrawable(null);
}
xml布局文件中也有可能会存在多余的背景设置 android:background
,如果你打算选用xml布局的背景,除了上面所说的将 android:windowBackground
设置为null外,也可以将它设置为完全透明:
<resource>
<style name="Theme.NoBackground" parent="android:Theme">
<item name="android:windowBackground">@android:color/transparent</item>
</style>
</resources>
常见的移除背景的地方比如 ImageView
背景,在列表显示图片前一般会需要显示一个默认占位符,当请求到网络图片要将它显示出来时,ImageView
的默认占位符背景图片应该被移除或设置为transparent透明,这样就能避免过度绘制问题:
4.3.2 View重叠
上面的卡牌View,除了最后的一张牌外,其他都是绘制一部分,另一部分被遮挡。如果还是按照正常绘制每张牌,不可见的部分就是过度绘制,我们需要去除它。
解决这个问题可以使用Android提供的API canvas.clipRect()
,前面的卡牌只截取可见的部分,这样就能避免过度绘制的问题。
// 处理前
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mDroids.length > 0 && mDroids.size() == mDroids.length) {
for (int i = 0; i < mDroidCards.size(); i++) {
// 每一张卡牌平移的位置
mCardLeft = i * mCardSpacing;
// 绘制整张卡牌
drawDroidCard(canvas, mDroidCards.get(i), mCardLeft, 0);
}
}
}
// 处理后
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mDroids.length > 0 && mDroids.size() == mDroids.length) {
int i;
for (int i = 0; i < mDroidCards.size() - 1; i++) {
mCardLeft = i * mCardSpacing;
canvas.save();
// 只绘制截取卡牌可见部分
canvas.clipRect(mCardLeft,
0,
mCardLeft + mCardSpacing,
mDroidCards.get(i).getHeight());
drawDroidCard(canvas, mDroidCards.get(i), mCardLeft, 0);
canvas.restore();
}
// 绘制最后一张完全可见的卡牌
drawDroidCard(canvas, mDroidCards.get(mDroidCards.size() - 1),
mCardLeft + mCardSpacing, 0);
}
}
4.3.3 9-patches
系统针对 9-patches
进行了特殊的优化(在9-patches图中,被其他视图所遮盖的部分,会被Android 2D渲染器优化为透明的),而透明像素部分不会被系统渲染绘制。因此,对背景图使用9-patches,可以在一定程度上减少过度绘制。
5 渲染管道中的CPU部分
为了在屏幕上显示图像,Android通常将xml文件转换为GPU能够识别的对象,然后显示在屏幕上,这个操作是在 DisplayList
的帮助下完成的。
DisplayList
持有所有要交给GPU绘制到屏幕上的数据信息,包含GPU需要绘制的全部对象的信息列表,还有执行绘制操作的OpenGL命令列表。在某个View第一次需要被渲染时,DisplayList
会因此被创建,当这个View需要显示在屏幕上时,我们将绘制指令提交给GPU来执行 DisplayList
。
我们下次渲染这个View时,比如说位置发生了变化,我们仅需要执行 DisplayList
就够了。
但是如果你修改了View的某些可见组件的内容,那么之前的 DisplayList
就无法继续使用了,这时我们要重新创建一个 DisplayList
,重新执行渲染指令并更新到屏幕上。
需要注意的是,任何时候View的内容发生变化,都需要重新创建 DisplayList
并重新执行指令更新到屏幕,这个流程表现的性能取决于你的View的复杂程度,取决于视觉变化的类型,同时对渲染管道也会产生一些影响。
比如,某个文本框尺寸变成当前的两倍,在改变尺寸前,需要通过父View重新计算并摆放其他子View的位置,这种情况下,我们改变了View,后面就会有很多工作要做。
当View尺寸发生变化和位置发生变化会触发测量和布局操作操作,会经过整个View Hierarchy询问各个View的尺寸和位置。这些处理的耗时是和View Hierarchy的View节点数量成正比的。
而这个问题的主要原因,就是View Hierarchy中包含太多的无用View,需要我们减少View的层级嵌套。
6 减少布局层级嵌套
减少布局层级嵌套的实战操作可以参考文章:Android一些你需要知道的布局优化技巧。
6.1 ConstraintLayout
当某个view的属性发生变化(比如TextView的内容发生变化),view自身会调用 view.invalidate()
方法,自底向上传播该请求,直到根布局,根布局会计算处需要重绘的区域,进而对整个布局层级中需要重新绘制的部分进行重绘。这一过程反复出现会影响UI的绘制时间,布局层级越复杂,UI加载的速度就越慢。所以扁平化布局非常重要。
ConstraintLayout 约束布局是Google提供的新控件,使用它可以在处理一些复杂的布局时有效的减少层级嵌套达到扁平化的目的,在目前最新的Android Studio,当你创建一个新的xml布局文件时,默认就会推荐使用约束布局。
在条件允许的情况下,尽可能的使用ConstraintLayout,而非LiearLayout。
6.2 布局标签
<include>
在某些情况下,当你希望在其他布局中重用一些已存在的布局时,<include>
标签可通过指定相关的引用id,将一个布局添加到另一个布局中。一般配合 <merge>
标签一起使用,减少布局层级冗余。
<merge>
系统会忽略 <merge>
标签,并将 <merge>
标签中的视图直接放置在相应的布局文件中。<merge>
标签只能作为布局的根来使用
每次在调用 LayoutInflater.inflate()
时,必须为 <merge>
布局文件提供一个view,作为它的父容器:
LayoutInflater.from(parent.getContext()).inflate(R.layout.merge_layout, parent, true);
<ViewStub>
ViewStub
可以作为一个节点,被添加到布局文件中。ViewStub
关联着一个布局文件,但它所关联的布局,直到运行时,通过调用 ViewStub.inflate()
或者 View.setVisibility()
方法,才会被绘制。
<ViewStub
android:id="@+id/viewstub"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inflatedId="@+id/inflated_id"
android:layout="@layout/viewstub_layout"/>
((ViewStub)findViewById(R.id.viewstub)).setVisibility(View.VISIBLE);
或
View newView = ((ViewStub)findViewById(R.id.viewstub)).inflate();
一旦 ViewStub
变为visible或者被inflate,它便不再可用,因为它在布局层级中的位置被实例化出来的布局所替代,因此不能再被访问,而应该使用 android:inflatedId
属性中的id。
ViewStub
只能inflate一次,之后ViewStub
对象会被置为空;当ViewStub
加载后,ViewStub
装载的layout的id就是inflatedIdViewStub
只能用来inflate一个布局文件,而不是某个具体的view
比较常见的 ViewStub
使用场景是,列表空界面、显示超时重试界面等,其他可以延后加载的控件布局都可以考虑使用 ViewStub
处理。
7 硬件加速
Android的渲染绘制分为软件绘制和硬件加速绘制两种。从图上看到,软件绘制使用的是 Skia 库,它是一款能在低端设备如手机上呈现高质量的 2D 跨平台图形框架,类似 Chrome、Flutter 内部使用的都是 Skia 库。
7.1 软件绘制
在Android 3.0之前,或者没有启动硬件加速时,系统都会使用软件方式来渲染UI。
整个流程如上图所示:
- Surface:每个
View
都由某一个窗口管理,每一个窗口都关联有一个Surface
- Canvas:通过
Surface
的lock
函数获得一个Canvas
,Canvas
可以简单理解为Skia底层接口的封装 - Graphic Buffer:
SurfaceFlinger
会帮偶们托管一个BufferQueue
,我们从BufferQueue
中拿到Graphic Buffer
,然后通过Canvas
以及Skia将绘制内容栅格化到上面 - SurfaceFlinger:通过
Swap Buffer
把Front Graphic Buffer
的内容交给SurfaceFlinger
,最后硬件合成器Hardware Composer
合成并输出到显示屏
CPU对于图形处理并不是那么高效,整个渲染流程完全没有利用到GPU的高性能。
7.2 硬件加速绘制
Android3.0(API 11)中引入了硬件加速,让视图以及所有Canvas对象的绘制操作,原本有CPU来完成,现在都交由GPU来完成。在Android4.0(API 14)以上默认开启。
硬件加速绘制的核心是通过GPU完成 Graphic Buffer
的内容绘制,此外硬件绘制还引入了 DisplayList
的概念,每个View内部都有一个 DisplayList
,当某个View需要重绘时,将它标记为 Dirty
。
当需要重绘时,仅仅需要重绘一个View的 DisplayList
,而不是像软件绘制那样需要向上递归。这样可以大大减少绘图的操作数量,因而提高了渲染效率。
7.3 硬件加速的限制
每个view都能被渲染并保存为一个离屏位图(off-screen bitmap),以供未来使用。首先通过调用 Canvas.saveLayer()
方法,渲染并保存位图,接着通过 Canvas.restore()
方法将所保存的位图绘制回画布中。
从Android3.0(API 11)开始,在创建离屏位图时,可通过 view.setLayerType()
方法选择图层类型。图层类型如下:
- View.LAYER_TYPE_NONE:不应用图层,正常渲染view,view不能被存储为一个离屏位图,这是默认行为。
- View.LAYER_TYPE_SOFTWARE:强制使用基于软件的绘制模型对view进行渲染,哪怕硬件加速是开启状态。用于以下情况:
- 需要对视图进行颜色过滤、混合或者应用透明度,以及应用程序不需要使用硬件加速的情况。
- 硬件加速是开启的状态,但遇到了渲染问题,这可作为一种兼容手段,让程序正常运行。
- View.LAYER_TYPE_HARDWARE:如果应用程序开启了硬件加速,那么view将在硬件中进行渲染,没有开启则和
View.LAYER_TYPE_SOFTWARE
一样
对于硬件绘制,我们是通过调用 OpenGL ES
接口利用GPU完成绘制,但硬件绘制目前并不能完全兼容,有部分Android API会被限制,其实这主要是受 OpenGL ES
版本与系统支持的限制,对于不支持的API,我们需要使用软件绘制模式,渲染的性能将会大大降低。
具体被限制的API可以参考官方文档:硬件加速