性能优化步骤

刚接触Android开发的时候时不时地会听到帧率,60fps这些名词,当时没放在心上但是后来在工作逐渐深入的情况下,接触到了性能优化的问题,测试有的时候会抱怨apk十分卡顿,软件项目经理看到性能测试报告后会找来说怎么占用这么多资源。这时候就需要我们针对具体情况对Apk进行性能优化了。
性能优化一般分成三步:

  • 首先初步定位当前要优化的问题是属于哪一类?UI渲染?内存?电量?网络?这个一般很容易从表象看出个大概,但是要注意这里有些因素是相互影响的,比如网络有可能导致电量消耗过快,内存抖动有可能照成界面卡顿。
  • 其次确定选择什么工具对性能进行测试,在这方面虽然工具较多,但是每个方面的性能测试工具一般都有那么几款主流的的工具,最主要的就是要知道我们要获取哪些指标,也就是在收集数据之前要明确我们要收集哪些数据。
  • 再其次就是使用工具对Apk的某项指标进行收集。
    有了数据最难的问题也来了—如何分析这些数据,这部分一般要求你对代码比较了解,这也是为什么一般性能优化不会让一个刚刚接触某个模块的人来完成。最后就是着手进行优化了,这一步要注意的就是权衡二字,看下改动是否是伤筋动骨的,如果是那种改动十分大的,就需要考虑是否值得开展,或者在临时版本上进行,优化结束后让测试介入进行专项测试。

性能优化学习资料推荐

关于性能优化主要分成如下几类:UI渲染,内存优化,电量优化,网络优化,代码级别优化,apk资源优化。这里面有些优化还具有相互关联的关系,比如电量优化和网络优化之间存在着十分密切的关联。接下来的几篇文章将把当时学习性能优化时候的总结分享出来,如果大家想从原始的资源进行学习我这边可以推荐大家如下的资源:

  1. yutobe 下搜索 Android性能优化典范 这个是google 提供的是比较权威的学习材料
  2. 优达的Android 系统性能 https://cn.udacity.com/course/android-performance--ud825/ 这个也是google推出的。
  3. 胡凯的Android性能优化典范的学习笔记 http://hukai.me/blog/archives/

UI视图优化

UI是一个Apk的脸面,在当前看脸的时代在很大程度上决定了Apk的命运。但是当当是漂亮还不行还需要流畅,你也许会常常听到用户抱怨在执行动画或者滑动ListView的时候感知到卡顿不流畅,其实这是因为这里的操作相对复杂,容易发生丢帧的现象,从而带来卡顿的感觉。

丢帧的原因很多:

  • 在主线程执行耗时操作,比如文件读写等操作。这个一般比较好被发现,用StrictMode一般都可以发现。
  • 一种是由于Layout太过复杂,布局嵌套节点太多,无法在16ms内完成渲染。
  • 一种可能是UI上有层叠太多的绘制单元,造成Over Draw。
  • 还有一种可能是动画执行的次数太多,动画过于复杂。
  • 内存频繁触发GC,照成内存抖动

但是最根本的原因是由于复杂的布局和动画导致CPU和GPU无法在16ms时间内完成当前页面的绘制与渲染,从而导致下一帧来的时候来不及绘制导致了丢帧的问题。接下来的部分将从上述的这几个方面对UI视图渲染优化进行讲解。主要涉及如下几个关键点:

  1. 为什么要保证所有绘制任务要在16ms内完成
  2. Android 系统UI绘制原理
  3. 影响UI视图渲染效率的原因
  4. 有那些测量工具和解决的方案

为什么要保证所有绘制任务要在16ms内完成

这里要求的60fps不是无故规定的,这是因为人眼与大脑之间的协作无法感知超过60fps的画面更新,所以大于60fps是没有必要的,这里有几个比较常见的帧率:12fps大概类似手动快速翻动书籍的帧率,这明显是可以感知到不够顺滑的。24fps是电影胶圈通常使用的帧率,这个速率能使得人眼感知当前的画面是连续线性的运动,但是只能足够支撑大部分电影画面需要表达的内容,还是达到顺畅的标准,一般而言低于30fps是无法顺畅表现绚丽的画面内容的,此时就需要用到60fps来达到想要的效果。
要使得帧率大于60fps就要求每个绘制任务必须要在16ms内完成,否则就会出现丢帧的现象:
下面两图分别是不丢帧的情况以及丢帧的情况:

下图的第二帧更新时间要花费24ms导致了下一帧的绘图信号来的时候当前更新还未完成导致丢帧,这样在下一帧该显示的时候依旧停留在当前帧。从而在视觉上会造成卡顿的现象。当然更新任务花费的时间越短越好,因为在主线程中除了界面更新外还有用户交互等任务需要处理。

从刷新频率和帧率角度看丢帧现象:

Refresh Rate:刷新率代表了在一秒内刷新屏幕的次数,这是由硬件决定的,一般会有个范围可以调整。
Frame Rate:帧率代表了GPU在一秒内绘制操作的帧数,例如30fps,60fps。
理想的情况下两个速率应该是一致的。但是在现实情况下帧率往往小于刷新频率。在这种情况下,某些帧显示的画面内容就会与上一帧的画面相同。这也就是常说的丢帧的问题。

Android 系统UI绘制渲染原理

UI 的绘制渲染任务主要是由CPU和GPU两大组件来完成的,CPU负责包括Measure,Layout,Record,Execute的计算操作,GPU负责Rasterization。下图描述了CPU,GPU的Pileline主要的问题,解决问题的工具以及解决问题的方案。

从图上可以看出CPU负责将需要绘制的UI组件计算成Polygons,Texture,然后交给GPU,GPU对其进行快速栅格化,也就是把Button等组件进行拆分到不同的像素上。


通过GPU栅格化后最终才绘制到屏幕上,为了能够使得我们的App运行得顺畅,不丢帧,需要保证在16ms内CPU 和 GPU 完成上述的计算,绘制,渲染等全部操作。

视图的渲染

对于布局,在进行绘制的时候Android需要在DisplayList的帮助下把XML布局文件转换成GPU能够识别并绘制的对象。DisplayList持有所有将要交给GPU绘制到屏幕上的数据信息。
在某个View第一次需要被渲染时,Display List会因此被创建,当这个View要显示到屏幕上时,我们会执行GPU的绘制指令来进行渲染。如果只是View的Property属性发生了改变(例如移动位置),我们就仅仅需要Execute Display List就够了。

但是如果View中的某些可见部分发生了变化那么就需要重新创建一个DisplayList,而不能继续使用原先的DisplayList了。

需要注意的是任何时候View中的绘制内容发生变化时,都会需要重新创建并渲染DisplayList,并将渲染后的结果更新到屏幕上。举个例子,假设某个Button的大小需要增大到目前的两倍,在增大Button大小之前,需要通过父View重新计算并摆放其他子View的位置。修改View的大小会触发整个HierarcyView的重新计算大小的操作。如果是修改View的位置则会触发HierarchView重新计算其他View的位置。如果布局很复杂,这就会很容易导致严重的性能问题。

影响UI视图渲染效率的原因:

  • CPU部分的问题—布局的层级太深
    最理想的布局在Hierarchy View 上是呈现扁平状的,也就是布局的层级结构不能太深,不要使用太多的布局嵌套关系。这部分的优化可以使用Hierarcy Viewer进行优化。

  • GPU 部分的主要问题有两个方面:

  1. Overdraw过度绘制
  2. 渲染太过耗时
    Overdraw指的是屏幕上的某些像素块在同一帧的时间内被绘制了多次。在多层次重叠的UI结构里面,如果对不可见的UI也进行绘制操作,就会导致某些像素区域被绘制了多次。从而浪费CPU以及GPU的时间。

但是从布局代码上很难直观得找出哪些像素块发生了Overdraw现象,如果要查看UI上的Overdraw情况可以打开Settings开发者选项的Show GPU Overdraw开关进行观察。

蓝色,淡绿,淡红,深红代表了4种不同程度的Overdraw情况,我们优化的目标就是尽量减少红色的区域。

具体的优化有如下的方案:
移除默认的Background
移除XML布局文件中非必需的Background
按需显示占位背景图片(也就是在ListView每个Item的ImageView上不事先显示占位图片,先去获取要显示的图片如果获取失败的时候再使用占位图片来显示在ImageView上)
对于复杂的自定义View由于Android系统无法检测在onDraw里面具体会执行什么操作,这就导致难以避免Overdraw。但是这个问题并不是完全没有办法解决,Android提供了canvas.clipRect()方法来帮助系统识别那些可见的区域。使用canvas.clipRect()方法可以指定一块矩形区域,只有在这个区域内的View才会被绘制,其他的区域的View都将被忽略。这种方法极大地避免了CPU与GPU资源的浪费。

下面是使用clipRect来解决自定义View Over Draw的例子,首先我们来看下原始方案的效果,在Over Draw 开启的情况下在层叠部分显示了大面积的红色区块,这是不可取的。

我们查看下该自定义View的onDraw方法:
代码逻辑很简单就是每隔一段偏移绘制一个图片。


下面是使用clipRect()方法来解决自定义View OverDraw问题的方案以及效果:



从效果上看红色区块已经完全去掉了。

使用Profile GPU Rendering 查看GPU渲染信息:

打开手机Settings里面的开发者选项,选择Profile GPU Rendering,选中On screen as bars的选项。就可以在手机屏幕上看到GPU Rending信息。

随着界面的刷新,界面上会用实时滚动的垂直柱状图来表示每帧画面所需要渲染的时间,柱状图越高表示花费的渲染时间越长。

界面上使用一条绿线表示16ms,如果柱状图的某个项超过这条横线那么就有可能导致丢帧照成的卡顿现象的发生。

从Android M系统开始,系统对GPU Profiling 功能进行了增强,早期的CPU Profiling工具只能粗略的显示出Process,Execute,Update三大步骤的时间耗费情况。下面是对应的视图情况。

在Android M版本开始,GPU Profiling工具把渲染操作拆解成如下8个阶段:

旧版本中提到的Proces,Execute,Update还是继续得到了保留,他们的对应关系如下:

接下去我们看下其他五个步骤分别代表了什么含义:
Swap Buffers 对应的是之前的Process阶段,表示渲染引擎执行显示列表所花的时间,view越多,时间就越长。
Command Issue对应的是之前的Execute阶段,表示把一帧数据发送到屏幕上排版显示实际花费的时间。其实是实际显示帧数据的后台缓存区与前台缓冲区交换后并将前台缓冲区的内容显示到屏幕上的时间。所以这个时间,一般都很短。
Draw 对应的是之前的Update阶段,表示在Java中创建显示列表部分中,OnDraw()方法占用的时间。
Sync & Upload:通常表示的是准备当前界面上有待绘制的图片所耗费的时间,为了减少该段区域的执行时间,我们可以减少屏幕上的图片数量或者是缩小图片本身的大小。
Measure & Layout:这里表示的是布局的onMeasure与onLayout所花费的时间,一旦时间过长,就需要仔细检查自己的布局是不是存在严重的性能问题。
Animation:表示的是计算执行动画所需要花费的时间,包含的动画有 ObjectAnimator,ViewPropertyAnimator,Transition等等。一旦这里的执行时间过长,就需要检查是不是使用了 非官方的动画工具或者是检查动画执行的过程中是不是触发了读写操作等等。
Input Handling:表示的是系统处理输入事件所耗费的时间,粗略等于对于的事件处理方法所执行的时间。一旦执行时间过长,意味着在处理用户的输入事件的地方执行了复杂的操作。
Misc/Vsync Delay:如果稍加注意,我们可以在开发应用的Log日志里面看到这样一行提 示:I/Choreographer(691): Skipped XXX frames! The application may be doing too much work on its main thread。这意味着我们在主线程执行了太多的任务,导致UI渲染跟不上vSync的信号而出现掉帧的情况。

Hierarchy 的使用:

Hierarchy 工具位于sdk的tool目录下,如果你使用的是Android Studio的话需要点击Android Device Monitor 工具进入。插入手机后会在左边的面板上会显示各个Activity的名字,点击要测试的Activity的名称就可以进入这个Activity的层级视图树。

通过这个视图树可以很明显得看出整个页面布局的视图结构,一般理想的布局的视图树是扁平的。
Save as PNG用来把当前视图树保存为一张png图片
Capture Layers用来分层保存
Load View Hierarchy用来手动刷新变化
Profile Node:用于触发测试。获取某个节点View的个数,Measure,Layout,Draw各个步骤所消耗的时间。这里通过红,黄,绿三种不同的颜色来区分布局的Measure,Layout,Executive的相对性能表现
右侧显示选中View的当前属性状态
右下角以红色框的形式显示当前View在Activity中的位置
其他的按钮也是很常见的大家可以自己探索下。
我们重点看下每个节点的视图信息部分:
在上面提到了点击Profile Node会触发测试,点击某个节点将会出现如下界面:

上图最底那三个彩色灯代表了当前View的性能指标,从左到右依次代表测量、布局、绘制的渲 染时间,红色和黄色的点代表速度渲染较慢的View
在自定义View的性能调试时,HierarchyViewer上面的invalidate Layout和requestLayout两个功能十分有帮助,我们可以在我们自定义View的代码上打上断点使用这两个按钮就可以执行invalidate()和 requestLayout()过程。

命令行方式获取帧率:

  1. 点击Android设备的“设置”->”开发者选项”,然后点击“GPU 呈现模式分析”,这时候会弹出一个对话框对话框中有三个选项,选择“在adb shell dumpsys gfxinfo中”这一项。
  2. 重启待测试的应用。在应用的页面上做要测试的动作。
    3.打开命令行工具,在命令行输入:adb shell dumpsys gfxinfo “apk名字”
    4.找到Profile data in ms这部分数据就是当前的各个阶段的帧率。
Contents
  1. 1. 性能优化步骤
  2. 2. 性能优化学习资料推荐
  3. 3. UI视图优化
    1. 3.1. 为什么要保证所有绘制任务要在16ms内完成
      1. 3.1.1. 从刷新频率和帧率角度看丢帧现象:
    2. 3.2. Android 系统UI绘制渲染原理
    3. 3.3. 视图的渲染
    4. 3.4. 影响UI视图渲染效率的原因:
    5. 3.5. 使用Profile GPU Rendering 查看GPU渲染信息:
    6. 3.6. Hierarchy 的使用:
    7. 3.7. 命令行方式获取帧率: