本篇文章已授权微信公众号 Android订阅 发布
简介
我们在开发的过程中,可能经常会遇到测试的一些反馈,就是APP运行卡顿的问题。我们通常所讲的卡顿问题都是因为渲染掉帧的问题引起视觉上的卡顿感。所以了解渲染机制,我们在项目的开发过程中,可以有意识的少挖坑。同时要打造一款精品的应用,注意渲染优化也是非常重要的一件事情。
当然目前我们好多同学在开发的工程中,经常会忽略渲染优化这一块,主要的原因可能是
- 项目没要求,能满足功能则可
- 缺少意识,没有做性能优化的意识
- 缺少用工具分析,主观感受不强
- 需求的苦海,无法脱身(有多少童鞋戳中泪点)
不管如何,我们都需要对自己有所要求。尽量在开发的过程中注意,少挖坑。对已上线的项目能够进行优化分析,打造精品。
接下来我们将介绍渲染的底层机制,并针对性地进行优化分析。
渲染机制
视觉感官
我们都可能听过Android的屏幕刷新频率是60fps 也就是16ms需要完成一帧的刷新。
首先我们理解一下帧的概念。
每一帧都是静止的图象,快速连续地显示帧便形成了运动的假象,因此高的帧率可以得到更流畅、更逼真的动画。
当物体在快速运动时, 当人眼所看到的影像消失后,人眼仍能继续保留其影像1/24秒左右的图像,这种现象被称为视觉暂留现象。是人眼具有的一种性质。人眼观看物体时,成像于视网膜上,并由视神经输入人脑,感觉到物体的像。但当物体移去时,视神经对物体的印象不会立即消失,而要延续1/24秒左右的时间,人眼的这种性质被称为“眼睛的视觉暂留”。
所以以前我们看胶卷电影的时候刷新的频率就是24fps。我们看起来就是连续的一个视觉效果。当然这里越高的帧率,我们可以得到更流畅、逼真的画面。
VSYNC
Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染, 如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms内完成。如果超过了16ms那么可能就出现丢帧的情况。
VSYNC有两个概念
- Refresh Rate:屏幕在一秒时间内刷新屏幕的次数----由硬件的参数决定,比如60HZ.
- Frame Rate:GPU在一秒内绘制操作的帧数,比如:60fps。
通常来说,帧率超过刷新频率只是一种理想的状况,在超过60fps的情况下,GPU所产生的帧数据会因为等待VSYNC的刷新信息而被Hold住,这样能够保持每次刷新都有实际的新的数据可以显示。但是我们遇到更多的情况是帧率小于刷新频率。在这种情况下,某些帧显示的画面内容就会与上一帧的画面相同,造成卡顿的现象。
简单来说,VSYNC也叫垂直刷新,是一个信号。会触发渲染。这个过程需要我们屏幕的刷新频率(一般60fps)和我们GPU所产生的帧数能够进行同步,那么UI的渲染就能流畅。如果我们自己定义的布局或者自定义控件的渲染时间超过了16ms每帧,那么就可能导致屏幕刷新的时候,我们的GPU还不能产生新的帧,用户看的还是旧的帧。这就造成了我们视觉上的卡顿,影响用户体验。
渲染管线
我们定义好了一个xml的布局界面后,是怎样最终呈现在我们的手机屏幕上的呢?
这里我们借助Google官方的性能优化的一张示例图来说明。
CPU负责把UI组件计算成Polygons,Texture纹理,然后交给GPU进行栅格化渲染。最终在屏幕进行显示。
这个地方CPU主要是将我们的布局文件的View Tree进行测量和绘制,最后形成Ploygons(多边形)及Texture(纹理贴图)
栅格化是绘制那些Button,Shape,Path,String,Bitmap等组件最基础的操作。它把那些组件拆分到不同的像素上进行显示。这是一个很费时的操作,GPU的引入就是为了加快栅格化的操作
Android在性能优化已经做了很多工作。在CPU将Ploygons和Texture传递到GPU是一个很耗时的过程。所以Android将Bitmaps,Drawables都是一起打包到统一的Texture纹理当中,然后再传递到 GPU里面,这意味着每次你需要使用这些资源的时候,都是直接从纹理里面进行获取渲染的。
Tip
项目里曾经遇到一个问题,对一个图标染色了。然后其他使用到改图标的地方也同样变成染色后的图标了。这个地方就是因为GPU有缓存的缘故。还有遇到过另外一个坑就是染色后的图标再红米的一个手机上无效,估计这个地方不同的硬件缓存的机制可能还不一样。所以如果项目中有用到图标的染色需要注意。
如何在我们的项目中进行渲染优化?
知道了我们的渲染的机制,我们知道整一个渲染的的流程,基本都是系统在处理,流程我们没办法进行干预。那么我们就需要针对渲染的原理,进行一些针对性的优化操作,减少我们每一帧的渲染时间,使应用更加流畅。所以平时除了我们都知道的阻塞UI线程导致卡顿,其实对于CPU及内存的不合理使用,也同样会造成我们的卡顿。接下来我们来一一进行分析。
内存优化
程序在任意帧内执行GCs所用的时间越多,消除少于16毫秒的呈像障碍,所必需的时间就会变少,如果有许多GCs或一大串指令一个接一个地操作,帧象时间很可能会超过16毫秒的呈像障碍,这会导致隐形的碰撞或闪躲。内存在断时间的抖动也会造成我们的卡顿现象。
所以如果要减少任意帧内启动GC的次数,需要着重优化程序的内存使用量。
我们在实际的项目中了已通过Monitor进行内存的抖动分析,再通过分析源码来看是否在某一时刻重复创建大量的对象,导致GC的回收。
Tip
- 避免在循环里面重复创建对象
- 操作大量的字符,慎用String进行+=,多使用StringBuilder及StringBuffer
- 多用池进行进行对象的复用
计算优化
这是一个很浅显的道理,我们知道渲染的过程需要CPU参与Ploygons与Texture的生成,假如我们将CPU的使用率长时间压榨得很高,自然就会影响我们的渲染,造成UI卡顿。
那么怎么来分析我们的计算优化呢?
首先一个很简单,可以看看是否在执行某个操作的时候,过分的压榨了CPU的使用率,我们通过Android Monitor可以看到瞬时的CPU的使用率。
观察到CPU使用率的异常后,我们可以通过Traceview工具来查找并确定哪些是阻碍应用程序性能问题的代码。
同样开DDMS视图选择我们要分析的应用,这里箭头所指向看上去像是三面箭头,上面有红色的圆点,如果按这些按钮,会出现一些提示,说将开始进行方法分析。这是TraceView的启动方法,我们点击它。将出现一个弹出窗口,提示有两种方法来分析你的应用程序。你可以记录每个方法的输入和输出,他们对资源的要求很高,或者,你也利用示例进行一些分析。其含义是,默认情况下分析程序,将会每1000毫秒侦测一次你的应用程序,以发现和记录实际上在运行的功能,现在,让我们来使用这些默认设置。我点击一下OK,既然分析程序已经在继续,我们就与你的应用程序进行交互,看能否记录一些动作。
我们来看跟踪视图,跟踪视图有两个主要组成部分。上方窗格的名称是timeline面板,下方窗格内有很多的信息,称为profile面板。这个时间线能够很好的显示代码的执行情况,这里显示的每一行,实际上对应于一个线程。显示的每一个颜色,对应于一个正在运行的特定方法。例如,我们可以看到,主线程的所有活动,我们可以看到方法启动和停止时间点,更有用的是放大这里,找到特定的方法,了解他们是如何执行的。它们会以这种U型模式显示出来。这里的条形表示,方法的启动时间。右侧的条形表示,方法的停止时间。条形的宽度表示方法执行所用的时间。现在,我们选择一个特定的方法,我们跳转到跟踪视图窗口的底部,这里,我们看到一些分析数据显示出来。我们可以看到哪些方法调用了我们选定的方法。
底部面板的一些字段含义如下:
列名 | 作用 |
---|---|
Name | 该进程运行过程中所调用的函数名 |
Incl Cpu Time | 函数占用的CPU时间,包含内部调用其它函数的CPU时间 |
Excl Cpu Time | 函数占用的CPU时间,但不包含内部调用其它函数所占用的CPU时间 |
Incl Real Time | 函数运行的真实时间(以毫秒为单位),内含调用其它函数所占用的真实时间 |
Excl Real Time | 函数运行的真实时间(以毫秒为单位),不包含调用其它函数所占用的真实时间 |
Calls+Recur Calls/Total | 函数被调用次数以及递归调用占总调用次数的百分比 |
Cpu Time/Call | 函数调用CPU时间与调用次数的比(该函数平均执行时间) |
Real Time/Call | 同CPU Time/Call类似,只不过统计单位换成了真实时间 |
Tip
- 优化一些计算的算法,例如递归等
- 使用线程池技术,避免过度压榨CPU
- 使用批处理及缓存,优化CPU计算
CPU优化
我们知道CPU在渲染的过程,主要需要处理Ploygons和Texture。在CPU方面,最常见的性能问题是不必要的布局和失效,这些内容必须在视图层次结构中进行测量、清除并重新创建,引发这种问题通常有两个原因:一是重建显示列表的次数太多,二是花费太多时间作废视图层次并进行不必要的重绘,这两个原因在更新显示列表或者其他缓存GPU资源时导致CPU工作过度。
引用Google官方示例图。
所以我们需要进行优化的点有:
- 减少不必要布局元素
- 减少过多的布局嵌套
那么如何来知道,我们的布局是否因为CPU过度工作导致我们的渲染卡顿呢?
我们可以通过DDMS里面的Hierarchy Viewer 来进行我们的布局分析。
1)通过AS的Tools-Android-Android Device Monitor调起
这个时候APP运行到我们需要检测的界面,这个点击蓝色的按钮,就可以显示当前界面的View Tree
2)我们可以通过图2箭头指向来观察我们的View布局、绘制、渲染的时间
- 箭头1为我们当前View节点的界面,我们可以观察当前节点的渲染时间
- 箭头2为触发检测渲染性能的按钮
- 箭头3为渲染性能的显示,有绿、黄、红三种颜色
三个圆点分别代表:测量、布局、绘制三个阶段的性能表现。
- 绿色:渲染的管道阶段,这个视图的渲染速度快于至少一半的其他的视图。
- 黄色:渲染速度比较慢的50%。
- 红色:渲染速度非常慢。
所以我们可以根据分析查看自己的布局,层次是否很深以及渲染比较耗时,然后想办法能否减少层级以及优化每一个View的渲染时。
Tip
- 避免过来无用的布局嵌套,特别是ViewGroup层级尽量最小化
- 使用<merge>标签,减少布局嵌套
- 使用懒加载布局 ViewStub,尽量减少使用View的GONE方式
- 注意一些自定义的View的性能,可通过工具的绿黄红分析
GPU优化
通过上面的流程我们知道,GPU主要干的事情就是栅格化,所以我们需要尽量尽量避免过度绘制(overdraw)。
我们在开发的过程中,经常会遇到牛逼的设计,需要完善绚丽的UI。高性能和完美的设计,往往会碰到一种性能问题,即过度绘制。过度绘制是一个术语,指的是屏幕上的某个像素点在同一帧的时间内被绘制了多次。假如我们有一堆重叠的UI卡片,最接近用户的卡片在最上面,其余卡片都藏在下面,也就是说我们花大力气绘制的那些下面的卡片基本都是不可见的。
我们借助Google官方的一个图来进行说明
Android在屏幕上使用不同颜色,标记过度绘制的区域,如果某个像素点只渲染了一次,我们看到的是它原来的颜色,随着过度绘制的增多,标记颜色也会逐渐加深,例如1倍过度绘制会被标记为蓝色,2倍、3倍、4倍过度绘制遵循同样的模式。所以当我们调试应用程序的用户界面时,目标就是尽可能的减少过度绘制,将红色区块转变成蓝色区块。
1)通过开发者选项打开过度绘制检测
2)开启后就可以查看应用的绘制情况
这里拿了百度网盘来做例子,还是优化得不错。
首先我们要从视图中清除那些,不必要的背景和图片,他们不会在最终渲染图像中显示,这些都会影响性能。其次,对视图中重叠的屏幕区域进行定义,从而降低CPU和GPU的消耗。
Tip
- 由于我们布局设置了背景,同时用到的MaterialDesign的主题会默认给一个背景。可以在Activity设置getWindow().setBackgroundDrawable(null);
- 尽量保持你的布局只有一层拥有Background,避免给过多的ViewGroup设置背景
- 如果是自定义控件可以通过裁剪来处理(Canvas.clipRect)。
总结
- 尽量了解渲染的机制,在开始做项目的时候就少挖坑
- 尽量动手给自己现在的项目进行优化,这样可以更深刻的理解
- 渲染优化是一个苦逼的体力活,掌握了方法以后,我们需要花时间去一个个调优