1.前言
在手机App竞争越来越激烈的今天,Android App的各项性能特别是流畅度不如IOS,安卓基于java虚拟机运行,触控响应的延迟和卡顿比IOS系统严重得多。一些下拉上滑、双指缩放快速打字等操作,安卓的流畅度都表现比较糟糕,但是,对于App使用过程是否流畅,一直没有一个可靠的指标将用户的客观感受和数据一一对应。虽然之前有FPS(每秒帧数)作为游戏或视频类App的性能指标,但对于那些界面更新不多的App来说,仍不是一个合适的衡量数据。以下会根据实际app性能测试案例,展开进行app性能评测之流畅度进行原理分析和评测总结。
1.1流畅度相关概念了解
刷新率vs帧率
刷新率:每秒屏幕刷新次数,手机屏幕的刷新率是60HZ
帧率:GPU在一秒内绘制的帧数
撕裂vs掉帧
撕裂
因为屏幕的刷新过程是自上而下、自左向右的,如果帧率>刷新率,当屏幕还没有刷新n-1帧的数据时,就开始生成第n帧的数据了,从上到下,覆盖第n-1帧。如果此时刷新屏幕,就会出现图像的上半部分是第n帧的,下半部分是第n帧的现象。CPU/GPU一直都在渲染。
丢帧
Android系统每隔16ms发出VSYNC信号,触发GPU对UI进行渲染,如果你的某个操作花费时间是24ms,系统在得到VSYNC信号的时候由于还没有准备好,就无法进行更新任何内容,那么用户在32ms内看到的会是同一帧画面(卡顿现象),即丢帧现象。
单缓存 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 的发生了!
1.2 VSync垂直同步
在Android版本更新过程中,发现在Jelly Bean中Google加入了一个Project Butter,用来解决严重影响Android口碑的问题之一“UI流畅性差”的问题。
而Project Butter中主要引入了三个核心元素:VSYNC(垂直同步)、Triple Buffer和Choreographer。
VSync是VerticalSynchronization(垂直同步)的缩写,是一种在PC上很早就广泛使用的技术,可以简单的把它认为是一种定时中断。而在Android 4.1(JB)中已经开始引入VSync机制。CPU和GPU的处理时间都少于一个VSync的间隔,即16.6ms。如果每个间隔都有绘制的情况下,当前的FPS即为60帧。VSync机制就像是播放动画片(60帧/s)。每次都会播放画面,有的时候有人偷懒了,机器坏了,就会出现播放速度降低的状况。我们把这个播放速度叫做流畅度。
1.3 理解FPS原理
FPS即Frames per second,>>点击这篇文章,解释的非常清楚。
用过flash的人应该知道动画片其实是由一张张画出来的图片连贯执行产生的效果,当一张张独立的图片切换速度足够快的时候,会欺骗我们的眼睛,以为这是连续的动作。反之类推,当你的图片切换不够快的时候,就会被人眼看穿,反馈给用户的就是所谓的卡顿现象。
想要让大脑觉得动作是连续的,至少是每秒10-12帧的速度,而想达到流畅的效果,至少需要每秒24帧。这也是为什么电影片源通常都是24帧的原因,好奇的同学点击
>>知乎高知
看看大神的解答。不过60帧每秒的流畅度是最佳的,我们的目标就是让程序的流畅度能接近60帧每秒,当然超过60帧速的话大部分人还是会受不了的。
系统获取FPS的原理是:1s 内 SurfaceFLinger 提交到屏幕的帧数。
计算公式:1000ms / 60 frames ≈ 16.67 ms/frames
原来的测试产品的流畅度,FPS是一个重要的指标,但是用了一段时间后,人们就发现了这样两个问题
Question:
1)为什么有时候FPS很低,但是我们却不觉得App卡顿?
2)App停止操作之后,FPS还是一直在变化,这样的情况是否会影响FPS的准确度?
后来测试人员分析了系统获取FPS的原理后,找到了那两个问题的答案:
Answer:
1)有时候FPS很低,我们却感觉不到卡顿,因为本来就用不到那么高的FPS,比如画一个动画只画了0.5秒就画完了,那么FPS最高也只有30帧/秒(标准是60帧/每秒),但这并不代表它是卡顿的,用0.5秒动画就画完了,不能为了凑够60帧/秒,在做个1s的动画吧。而如果屏幕根本没有绘制需求,即屏幕显示的画面是静止的,那FPS就为0。
2)App停止操作后FPS还一直变化,是因为屏幕每一帧的合成都是针对手机里的所有进程,那么即使你的App停止了绘制,手机里其他进程可能还在绘制,比如通知栏的各种消息,这会导致FPS继续变化。
2. 如何计算流畅度
从上一节的原理分析来看,对流畅度的评价没有一个既定的测量标准。不同的应用有相对适应的计算方式,总结如下:
系统合成帧率(FPS):数据形式最为直观(FPS 是最早的显示性能指标,而且在多个平台中都有着类似的定义),且对系统平台的要求最低(API level 1),游戏、视频等连续绘制的应用可以考虑选用,但不适用于绝大多数非连续绘制的应用;
流畅度(SM):数据形式与 FPS 类似,可以很好的弥补 FPS 无法准确刻画非连续绘制的应用显示性能的缺陷;
应用跳帧次数、幅度(Aggregate frame stats):除了对系统平台有较高的要求以外,其采集方式最为简单(系统自带功能);
丢帧(Skipped frames):与 Aggregate frame stats 类似, 信息量相对较少,但可适用范围更广
2.1 FPS获取
APP需要尽可能的超过24帧/秒,接近60帧/秒的速度,并且在使用的过程中保持这个速率,因此这意味着我们的程序需要在16.67ms内处理一幅画面内的所有事,并保持住这个状态。
计算公式:1000ms / 60 frames ≈ 16.67 ms/frames
2.1.1 GPU呈现模式分析
操作方法:通过 [设置]->[开发者选项]->[GPU呈现模式分析] ->[在屏幕上显示为条形图] 进行直观的取样,截图如下:
解读:屏幕下方的柱形图会持续刷新,最上方会有一根绿色的线,代表的是16ms的阈值,超过这个界限表示当前帧绘制的时间出现了延迟,及卡顿现象,后面会详细介绍原因。横坐标表示时间的持续,每一根柱形图表示当前帧的绘制时间。因此我们在使用的过程中,下面的柱形图会一直的刷新,单位是ms。各位看官是否有注意到每一帧的柱形图颜色不一样呢(注:不同手机的颜色不一样,仅限安卓4.0以上版本参考)
优点:可直接在手机上操作,方便直观
缺点:这个方式获取到的渲染时间只是UI主线程上的绘制行为。“GPU呈现模式分析”的数据只能说明个现象,比如上面提到的数据,能说明在实际运行中会有短暂的长时间绘制问题。但造成问题的具体原因并没有说明。
而且“GPU呈现模式分析”显示的是最后128帧的数据,但丢帧也有可能是两帧之间存在长时间的操作而造成的。
2.1.2 通过gfxinfo获取
操作:设备连接usb数据线,使用adb调试工具,随后对返回的数据进行适当处理便可以得到此时此刻app的fps。adb shell dumpsys gfxinfo yourpackagename
解读:
Draw:是消耗在构建java显示列表DisplayList的时间。说白了就是执行每一个View的onDraw方法,创建或者更新每一个View的DisplayList对象的时间。
Process:表示是消耗在Android的2D渲染器执行显示列表的时间,view越多,要执行的绘图命令就越多,时间就越长
Execute:消耗在排列每个发送过来的帧的顺序的时间.或者说是CPU告诉GPU渲染一帧的时间,这是一个阻塞调用,因为CPU会一直等待GPU发出接到命令的回复。所以这个时间,一般都很短。
Draw + Prepare+Process + Execute = 完整显示一帧 ,这个时间要小于16ms才能保存每秒60帧。
将数据复制到excel中,然后将数据生成“堆积柱形图”如下:
缺点: 这种方式是最普遍也是最常用的一种,但在使用上有明显的痛点,一是设备需要连接usb,二是必须是Android M 版本以上才支持,三是adb命令返回的数据并不是实时fps,需要经过处理才能得到,因此不能在测试app的过程中实时显示fps
2.1.3 通过系统层级SurfaceFlinger获取
原理:在 Android 系统中,SurfaceFlinger 扮演了系统中所有 Surface 的管理者的角色,当应用程序所对应的 Surface 更新之后,绝大多数的 Surface 都将在 SurfaceFlinger 之中完成了合并的工作之后,最终才会在 Screen 上显示出来。
知道android绘制原理的人应该能明白,SurfaceFlinger就是负责绘制Android应用程序UI的服务,所以surfaceFlinger能反应出整体绘制情况,一般正常情况都是连续的,如果出现空档,一种是没有操作或者滑动到头,没东西需要绘制,这种属于正常,另一种就是有问题存在,有其他操作时间过长。
操作:设备连接usb数据线,使用adb调试工具,adb shell dumpsys SurfaceFlinger packagename
2.2 Aggregate frame stats指标的计算方法
首先需要说明的是 Aggregate frame stats 不是一个指标,而是一系列指标集合。我们来看一个具体的 Aggregate frame stats 的例子:
Stats since: 752958278148ns
Total frames rendered: 82189
Janky frames: 35335 (42.99%)
90th percentile: 34ms
95th percentile: 42ms
99th percentile: 69ms
Number Missed Vsync: 4706
Number High input latency: 142
Number Slow UI thread: 17270
Number Slow bitmap uploads: 1542
Number Slow draw: 23342
以上统计信息的实现可以详见源码:GfxMonitorImpl.java
在 Android M 以上的系统上,上述信息的获取十分方便(事实上也只有这些系统能够获取这些信息)。仅需要执行以下命令即可:
adb shell dumpsys gfxinfo <PACKAGE_NAME>
优点:除了对系统平台有较高的要求以外,其采集方式最为简单(系统自带功能);
2.3 丢帧计算
在一次Loop时如果执行时间超过了16.6ms,那么用多于16.6ms的时间除以16.6ms,即是当前App的丢帧(SF: Skipped Frame)
在16.6ms完成工作却因各种原因没做完,占了后n个16.6ms的时间,相当于丢了n帧
故:
SF=处理帧数 / (处理帧数 + 额外的垂直同步脉冲) * 60 计算(其中处理帧数常为128)
这个指标的就是指当前应用在丢帧发生时的丢帧帧数。
针对 Logcat 方案, 该数值直接在 Logcat 中输出,并且带有时间信息。
04-18 16:31:24.957 I/Choreographer(24164): Skipped 4 frames! The application may be doing too much work on its main thread.
04-18 16:31:25.009 I/Choreographer(24164): Skipped 2 frames! The application may be doing too much work on its main thread.
针对 Choreographer.FrameCallback 方案 以及 代码注入方案,我们可能很方便的通过计算前后两帧开始渲染的时间差获得这一数值,同样方便。同样与 Logcat 方案 不同的是,它也是可以设计成实时计算的。
缺点:Android4.2+系统,适用于SW/HW Rendering 及 部分 OpenGL Rendering
2.4 流畅度计算
原理:VSync 机制就像一台转速固定的发动机(60转/s),每一转带动着做一些 UI 相关的事情。有时候因为各种阻力, 某一圈的工作量比较重, 超过了 16.6ms, 那么这一秒内就不是 60 转了。
我们通过测量这个转速,来评判应用的流畅度。
和丢帧相对,在VSync机制中1s内Loop运行的次数。和丢帧相对1s内有60个Loop因为某几次工作时间超过了16.6ms(丢帧),这样Loop就无法运行60次(理论最大值)。当流畅度越小的时候说明当前程序越卡顿。
计算方式:SM = 帧率(60) * (单位时长总帧数 - 单位时长丢帧数) / 单位时长总帧数
操作:
VSync机制客户通过其Loop来了解当前App最高绘制能力,其机制如下:
1)固定每隔16.6ms执行一次;
2)如果没有绘制事件的时候也会运行这样一个Loop;
3)Loop在1s之内运行了多少次,即可以表示当前App绘制的最高能力,也就是App卡顿的程度。
if(存在帧的绘制):
Loop = 1 帧绘制完成所占用的Vsync间隔
else:
Loop = 1个Vsync间隔
所以SM计算方法为Loop在1s内运行了多少次(Loops per seconds),那么我们可以直接在App代码中通过Choreographer的回调FrameCallback来计算Loop被运行了几次,从而知道应用的流畅度。但在实际情况下我们不一定能修改代码(实际发布的版本不允许加入测试代码)或者根本拿不到代码(譬如和竞品进行对比)。
所以介绍一种更简单直观测量Android应用流畅度的方法,就是通过开源测试工具GT(http://gt.qq.com)。
优点:数据形式与 FPS 类似,可以很好的弥补 FPS 无法准确刻画非连续绘制的应用显示性能的缺陷;
缺点:Android4.2+系统,适用于SW/HW Rendering 及 部分 OpenGL Rendering
3.卡顿问题分析工具
分析UI卡顿我们一般都借助工具,通过工具一般都可以直观的分析出问题原因,从而反推寻求优化方案,具体如下细说各种强大的工具
3.1 Hierarchy Viewer使用
我们可以通过SDK提供的工具HierarchyViewer来进行UI布局复杂程度及冗余等分析
通过命令启动HierarchyViewer
Hierarchyviewer
接下来Hierarchy window窗口打开:
一个Activity的View树,通过这个树可以分析出View嵌套的冗余层级,以及每个View在绘制的使用时长也有表示。
3.2 使用Lint进行资源及冗余UI布局等优化
冗余资源及逻辑等也可能会导致加载和执行缓慢,这可以使用Link工具,发现代码中的流畅度性能问题;
在AndroidStudio 1.4版本中使用Lint最简单的办法:就是将鼠标放在代码区点击右键->Analyze->Inspect Code–>界面选择你要检测的模块->点击确认开始检测,等待一下后会发现如下结果:
如果存在冗余的UI层级嵌套,会进行高亮显示, 我们根据提示可以点击跳进去进行优化处理掉的。
3.3 DDMS内存查看器
由于Android系统会依据内存中不同的内存数据类型分别执行不同的GC操作,常见应用开发中导致GC频繁执行的原因主要可能是因为短时间内有大量频繁的对象创建与释放操作,也就是俗称的内存抖动现象,或者短时间内已经存在大量内存暂用介于阈值边缘,接着每当有新对象创建时都会导致超越阈值触发GC操作
根据内存抖动现象,查看log日志进行分析:
如果看到,这种不停的大面积打印GC导致所有线程暂停的操作必定会导致UI视觉的卡顿,所以我们要避免此类问题的出现,具体的常见优化方式如下:
-
检查代码,尽量避免有些频繁触发的逻辑方法中存在大量对象分配;
-
尽量避免在多次for循环中频繁分配对象;
-
避免在自定义View的onDraw()方法中执行复杂的操作及创建对象(譬如Paint的实例化操作不要写在onDraw()方法中等);
-
对于并发下载等类似逻辑的实现尽量避免多次创建线程对象,而是交给线程池处理。
有了上面说明GC导致的性能后我们就该定位分析问题了,我们可以通过运行DDMS->Allocation Tracker标签打开一个新窗口,然后点击Start Tracing按钮,接着运行你想分析的代码,运行完毕后点击GetAllocations按钮就能够看见一个已分配对象的列表,如下:
3.4 Tracer for OpenGL ES
可以记录和分析APP每一帧的绘制过程,以及列出所有乃至的OpenGL ES 的绘制函数和耗时;该工具操作后会生成一份记录App绘制过程和gltrace文件。
3.5 BlockCanary分析android卡顿
在复杂的项目环境中,由于历史代码庞大,业务复杂,包含各种第三方库,所以在出现了卡顿的时候,我们很难定位到底是哪里出现了问题,即便知道是哪一个Activity/Fragment,也仍然需要进去里面一行一行看,动辄数千行的类再加上跳来跳去调来调去的,结果就是不了了之随它去了,实在不行了再优化吧。于是一拖再拖,最后可能压根就改不动了,客户端越来越卡。
Android应用卡顿是非常普遍的现象,偶尔出现ANR。只有当APP出现ANR,我们才能得到当前堆栈信息。当应用只是卡顿或只是不太流畅的时候,我们能不能找出卡顿元凶呢?不依赖Debug和源码的情况,能不能找出卡顿的堆栈信息呢?我们需要找到一种方法来检测哪些函数可能会使应用发生ANR,在开发阶段就能找出卡顿元凶,提高应用流畅度。
BlockCanary就是来解决这个问题的。告别打点,告别Debug,哪里卡顿,一目了然,让优化代码变得有的放矢。
具体使用方法请点击:
BlockCanary介绍 地址
BlockCanary — 轻松找出Android App界面卡顿元凶
4.XX银行-性能评测对比掉帧率案例
4.1 总览
此次质量开放平台-评测中心(http://fit-stg1.jryzt.com/Hyperion-server/html/index.html)的性能测试的流畅度测试主要是针对场景页面的掉帧率数据采集进行对比分析, 原理公式为:掉帧率=处理帧数 / (处理帧数 + 额外的垂直同步脉冲) * 60 计算(其中处理帧数常为128)。一般掉帧率超过10%,我们就认为存在卡顿有必要进行分析定位。
4.2 掉帧率对比分析案例
这里选取了同一家银行的两个APP与行业竞品进行掉帧率对比分析,从掉帧率对比看,行业竞品均值为4.1%,90分位约13.1%,75分位约27.5%,中位数约39.6%,25分位约59.9%。
【福建农信】掉帧率为1.783%,表现良好,打败了行业90%以上的竞品
【榕商Bank】掉帧率为6.244%,表现良好,打败了行业90%以上的竞品
整体得分对比分析:
从首页启动到加载完成场景分析,【福建农信】实际启动到首页场景只有一个简单的未登录页,相比于丰富多样的【榕商Bank】来说属于非常简单的页面,但是它的掉帧率与丰富资源的【榕商Bank】比较相差不远。
【福建农信】首页掉帧率问题分析:
单纯从页面表象观察,【福建农信】启动时,未登录页是从APP背景页下方飘进渐渐上升在页面中间,然后抖动一下再静止,有一种PPT飞入的动态效果。
通过深入分析得出【福建农信】应用交互中主线程存在卡顿,存在 Activity(LoginActivity)切换过慢的现象:
cn.com.fjnx.mobilebank.per.activity.account.LoginActivity.onCreate(阻塞1639 ms)
com.yitong.fjnx.mbank.android.Splash.onCreate(阻塞1717 ms)
建议【福建农信】优化启动时未登录页进入的方式
【榕商Bank】首页掉帧率问题分析:
首页加载掉帧率为8.2%,通过GPU过度绘制调试发现:com.pingan.fstandard.activity.MainActivity存在过度绘制。实际上是因为运营Banner位有轮播动态效果,轮播间隔时间设置的比较长,导致评测时掉帧率偏高,但是这是合理的产品设计,而且也不影响用户体验。
综上对比,【榕商Bank】流畅度表现优于【福建农信】,【福建农信】掉帧率仍然有优化空间。
5.App端卡顿问题排查思路
1)UI线程卡顿
问题:UI线程中有I/O读写、数据库访问等耗时操作,导致UI线程卡顿;
定位及解决:TraceView 寻找卡住主线程的地方,Systrace 获取 app 运行是线程的信息以及 API 的执行情况,避免在主线程执行 IO 操作。
2) 复杂、不合理的布局或过度绘制
问题:不合理的布局虽然可以完成功能,但随着控件数量越多、布局嵌套层次越深,展开布局花费的时间几乎是线性增长,性能也就越差;
定位及解决: 避免OverDraw导致的性能损耗;可以参考《Android性能优化(二)之布局优化面面观》
3)同一时间动画执行的次数过多
问题:同一时间动画执行的次数过多,导致CPU或GPU负载过重;
4) 内存使用异常导致卡顿
问题:内存抖动、内存泄漏都会导致:GC的次数越多、消耗在GC上的时间越长,CPU花在界面绘制上的时间相应越短;
解决:节省内存的分配空间,尽可能的降低GC的频率,缩短GC的平均时间;CPU不被占用,卡顿的几率就会更低; 可以参考《Android性能优化(四)之内存优化实战》
5) 冗余资源及逻辑等导致加载和执行缓慢;
问题:对线程开启方式的不同选择以及不同配置都可能导致卡顿的发生;
解决:任何耗时操作正确的移到异步里,类如I/O读写、数据库访问等都应该采用异步的方式,不能有“只是一个很小的文件”之类的想法,防微杜渐;
参考:
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使用详解