多数情況下,卡顿发生的根本原因,是渲染问题,即系统无法及时的完成复杂界面的渲染操作。系统会尝试每个16ms对UI进行渲染,如果每次都渲染成功,这样画面就是流畅的(达到了60fps);否则,就会发生丢帧现象,丢帧越多,用户感受到的卡顿情况就越严重。
为了获得更平滑的动画,就必须保证帧率不低于60fps——意味着每帧只能花费16毫秒的时间。
1、一些概念
1、刷新率vs帧率
- 刷新率:每秒屏幕刷新次数,手机屏幕的刷新率是60HZ
- 帧率:GPU在一秒内绘制的帧数
2、撕裂vs掉帧
-
撕裂
因为屏幕的刷新过程是自上而下、自左向右的,如果帧率>刷新率,当屏幕还没有刷新n-1帧的数据时,就开始生成第n帧的数据了,从上到下,覆盖第n-1帧。如果此时刷新屏幕,就会出现图像的上半部分是第n帧的,下半部分是第n帧的现象。CPU/GPU一直都在渲染
-
丢帧
Android系统每隔16ms发出VSYNC信号,触发GPU对UI进行渲染,如果你的某个操作花费时间是24ms,系统在得到VSYNC信号的时候由于还没有准备好,就无法进行更新任何内容,那么用户在32ms内看到的会是同一帧画面(卡顿现象),即丢帧现象。
3、单缓存 vs VSYNC vs 双缓存 vs 三缓存
- 单缓存(没有引入VSync )
GPU向缓存中写入数据,屏幕从缓存中读取数据,刷新后显示。由于刷新率和帧率并不总是一致的,很可能导致撕裂的现象。为了解决单缓存的画面撕裂问题,出现了双缓存和 VSync 。
-
VSYNC 和 双缓存
双缓存使用了两个缓存区: Back Buffer 、 Frame Buffer。当写入下一帧时,GPU会先填充 Back Buffer 中,当刷新屏幕时,屏幕从 Frame Buffer 中读数据。VSYNC 主要是完成帧的复制,开始下一帧的渲染。
当帧率大于刷新频率时,通过使帧率被迫跟刷新频率保持同步,从而避免画面撕裂的现象(只有当 VSync 信号产生时, CPU/GPU 才会开始绘制)。当VSync 信号产生时,先完成Back Buffer 到 Frame Buffer的复制操作(通过交换内存地址),然后通知 CPU/GPU 绘制下一帧图像。也只有VSync 信号发生时,才绘制下一帧。
当刷新频率>帧率时,此时刷新屏幕,发出VSYNC 信号,由于CPU/GPU的渲染操作还没有完成,就不把Back Buffer的数据复制到 Frame Buffer,此时就从Frame Buffer去取旧数据,这样在两个刷新周期里,显示的是同一帧数据, 掉帧
-
三重缓存
双重缓存的缺陷在于:当 CPU/GPU 绘制一帧的时间超过 16 ms 时,会产生 Jank。更要命的是,产生 Jank 的那一帧的显示期间,GPU/CPU 都是在闲置的。
如下图,A、B 和 C 都是 Buffer。
如果有第三个 Buffer 能让 CPU/GPU 在这个时候继续工作,那就完全可以避免第二个 Jank 的发生了!
2、渲染是怎么实现的
如何把布局渲染成用户可以识别的图片?它的核心是通过栅格化操作,将一些按钮、文字等布局对象拆分到像素点进行显示。由于栅格化是非常费时的,因此引入了GPU加快栅格化过程。
整个处理过程是:CUP负把把UI对象转变GPU可以识别的成图元(多边形、纹理),然后上传到GPU进行栅格化过程。
1、在GPU中保存图元
但是由于这个转换、上传都是耗时的,所以为了减少时间,需要减少它们的数量,庆幸的是Open GL API,允许你将这些图元保存到GPU,当下次需要一个按钮的时候,只要参考GPU中已经存在的图元,告诉GPU如何去绘制 它。优化渲染性能就意味着尽可能多且快的将更多的数据上传到GPU,然后留在GPU,尽可能长时间的不去修改它。
2、display list,CPU、GPU沟通的桥梁
将图元从CPU上传到GPU,这个操作是在DisplayList的帮助下完成的。DisplayList持有GPU渲染时需要的信息,包括了一些图元和Open GL命令列表。
CPU直接与GPU通信,而是通过中间的一个图形驱动层(Graphics Driver)来连接这两部分。 图形驱动维护了一个队列,CPU把display list添加到队列里,GPU从这个队列取出数据进行绘制。
Display List,在View第一次需要被渲染时被创建,当这个View要显示到屏幕上时,我们会执行GPU的绘制指令来执行这个Display List。
整体流程是:CUP负把把UI对象转变GPU可以识别的成图元(多边形、纹理),并存储进display list列表;GPU执行绘图指令来执行display list,取出相应的图元信息,进行栅格化渲染
3、引起的原因和检测方法
主要有以下几点,可能引起渲染的性能问题:
- 布局Layout过于复杂,无法在16ms内完成渲染。
- 同一时间动画执行的次数过多,导致CPU或GPU负载过重。
- View过度绘制,导致某些像素在同一帧时间内被绘制多次。
- UI线程中做了稍微耗时的操作。
可以通过如下工具进行检测
HierarchyViewer(布局尽量扁平化)、Show GPU Overdraw、Profile GPU Rendering等等
1、Profile GPU Rendering
1、打开手机里面的开发者选项,选择Profile GPU Rendering,选中On screen as bars的选项
通过Profile GPU Rendering,可以在屏幕上实时显示渲染每一帧图像花费的时间。渲染时间用柱状图表示,每一条柱状图都由3部分组成,蓝色、红色和黄色,代表渲染的3个不同的阶段,通过分析这三个阶段的时间就可以找到渲染时的性能瓶颈。
2、使用命令
adb shell dumpsys gfxinfo yourpackagename
将得到的数据保存为txt,之后导出到excel(数据-导入数据),生成图表
Draw:表示在Java中创建显示列表部分中,OnDraw()方法占用的时间。
Process:表示渲染引擎执行显示列表所花的时间,view越多,时间就越长
Execute:表示把一帧数据发送到屏幕上排版显示实际花费的时间。其实是实际显示帧数据的后台缓存区与前台缓冲区交换后并将前台缓冲区的内容显示到屏幕上的时间。所以这个时间,一般都很短。
Draw + Process + Execute = 完整显示一帧 ,这个时间要小于16ms才能保存每秒60帧(即1000秒/60帧)。
将数据复制到 excel中,然后将数据生成“柱形图”来观察结果。
3、通过Android Studio的GPU Monitor查看
4、分析
随着界面的刷新,界面上会滚动显示垂直的柱状图来表示每帧画面所需要渲染的时间,柱状图越高表示花费的渲染时间越长。中间有一根绿色的横线,代表16ms,我们需要确保每一帧花费的总时间都低于这条横线,这样才能够避免出现卡顿的问题。
每一条柱状线都都包含三部分
- 蓝色代表测量绘制Display List的时间,是CPU将UI对象转变成图元,并缓存在display list的时间
视图(display list )突然无效了 - 红色是GPU执行渲染指令,使用OpenGL执行Display List的时间
view过于复杂或绘制多次 - 黄色代表CPU等待GPU处理的时
GPU做了太多的工作
总之,布局层次过深,view过于复杂都会导致时间过长
Swap Buffers(黄色):CPU等待GPU的时间
Command Issue(红色):OpenGL绘制display list的时间
Sync/upload(浅蓝色):代表上传bitmap到GPU的时间,如果值比较大,是因为花费了相当多的时间价值大量图片。
Draw(蓝色):创建和更新display list的时间
Misc Time/Vsync Delay(深绿):在两个连续的帧之间执行的操作,如果很大,可能因为在UI线程做了太多的工作。
Input Handling(浅绿):处理用户的输入事件,花费的时间
Animation(浅浅绿):在当前帧中的动画 花费了的时间
2、过度绘制
过度绘制(Overdraw)是屏幕上的某个像素在同一帧的时间内被绘制了多次。可以在开发者选项中打开 Show GPU Overdraw,观察过度绘制情况。
如果没有颜色,代表没有过度绘制,蓝色,淡绿,淡红,深红代表了4种不同程度的Overdraw情况,分别表示了对应的像素被多绘制了1、2、3、4次,应当尽量减少红色区域。
过度绘制可能是因为组件的互相重叠或者不必要的背景重叠
3、HierarchyViewer
1、打开Hierarchy Viewer
点击 Tools>Android>Android Device Monitor,选择 Hierarchy Viewer,如下图:
但是Hierachy Viewer默认是无法连接真机调试,只能使用模拟器了。但是要APP先运行起来再使用Android Device Monitor。
2、分析页面布局性能
选中一个节点,点击 右上角,就可以获取到布局绘制的时间,如图
从左到右依次,代表View的Measure, Layout和Draw的性能,不同颜色代表不同的性能等级:
- 绿:该View的此项性能比该View Tree中超过50%的View都要快
- 黄:该View的此项性能比该View Tree中超过50%的View都要慢
- 红:该View的此项性能是View Tree中最慢的
4.2 测量结果分析
红色节点是代表应用性能慢的一个潜在问题,下面是几个例子,如何来分析和解释红点的出现原因?
1)如果在叶节点或者ViewGroup中,只有极少的子节点,这可能反映出一个问题,应用可能在设备上运行并不慢,但是你需要指导为什么这个节点是红色的,可以借助Systrace或者Traceview工具,获取更多额外的信息;
2)如果一个视图组里面有许多的子节点,并且测量阶段呈现为红色,则需要观察下子节点的绘制情况;
3)如果视图层级结构中的根视图,Messure阶段为红色,Layout阶段为红色,Draw阶段为黄色,这个是比较常见的,因为这个节点是所有其它视图的父类;
4)如果视图结构中的一个叶子节点,有20个视图是红色的Draw阶段,这是有问题的,需要检查代码里面的onDraw方法,不应该在那里调用。
- 布局常见问题与优化建议
1)没有用的父布局时指没有背景绘制或者没有大小限制的父布局,这样的布局不会对UI效果产生任何影响。我们可以把没有用的父布局,通过<merge/>标签合并来减少UI的层次;
2)使用线性布局LinearLayout排版导致UI层次变深,如果有这类问题,我们就使用相对布局RelativeLayout代替LinearLayout,减少UI的层次;
3)不常用的UI被设置成GONE,比如异常的错误页面,如果有这类问题,我们需要用<ViewStub/>标签,代替GONE提高UI性能。
参考:Android 性能模式 第一季、Android性能优化典范 - 第1季、Android性能优化之渲染篇、Android性能优化系列——Profile GPU Rendering、Profile GPU Rendering Walkthrough、Android 显示原理简介、Android 4.4 Graphic系统详解(2) VSYNC的生成、理解 VSync、了解Android 4.1,之三:黄油项目 —— 运作机理及新鲜玩意
、Hierarchy Viewer使用详解