安卓帧渲染数据获取方式小结
首先解释一下文章标题中的“帧渲染数据”。
“帧渲染数据”是指,完成渲染一帧的耗时。这是计算帧率的基础数据。
截止到 8.0 系统,安卓原生提供 API 或者自带的工具,甚至是统计性能的后台 Google Vitals,都没有提供直接获取帧率的功能。但是这些 API 或工具,直接或者间接的提供了获取每一帧渲染耗时的功能,开发者需要做二次计算才能得到帧率。至于为什么并给出帧率这个数据,我会在后文中给出自己的推测。
如果我们拿到了每一帧的耗时,我们就拿到了两个数据:某段连续时间 deltT 内渲染完成的帧数 n,那么 n / deltT 就是帧率。deltT 的选取上具有很大灵活性,deltT 应该设置为 1 秒,还是 2 秒?亦或是,n 固定为 1,相应的 deltT 设置该帧的耗时?不同的选取方法,得到的帧率值也不尽相同。比如第 n 帧耗时 tn,对 1/tn > 60 ? 60 : 1/tn 累加求和然后求均值,实际操作后会发现这种方案受某超时帧影响严重,如果某帧耗时较大,会大大拉低最后的 fps 值。
注意,虽然安卓原生系统没有直接提供帧率这个性能指标数据,但是某些第三方 Rom,比如魅族 M2 Note 手机上,Flyme 系统提供了帧率数据。
下面讲下获取帧数据的策略和对应的实现方式。
两种策略四种方式
目前,获取帧数据的策略由 Choreographer.FrameCallback 和 GraphicsBinder 两种。
Choreographer.FrameCallback 的代表作是开源库 TinyDancer 和美团外卖的 Hertz(卡顿侦测)。
GraphicsBinder 的代表方式是 Profile GPU 和 FrameMetrics。
下面分别进行介绍。
Choreographer$FrameCallback
这种方式起源于 Facebook 在 DroidCon 的分享:《Road to 60fps》。在这之后,基于这个思路获取帧数据的各种开源库便如雨后春笋般出现了。
从 16ms 说起
多数设备的屏幕刷新频率是 60Hz,即每秒刷新 60 次,每隔 16.67 ms 刷新一次。如果下一帧能够在 16.67 ms 内渲染完成,每次刷新都能展示新的帧,在用户看来 app 流畅运行,否则第 N+1 次屏幕刷新将继续展示第 N帧(第 N+1 帧尚未渲染完成),将出现掉帧、卡顿现象。
但是需要注意的是,并不是所有的设备的刷新频率都是 60hz,相应的 60fps 对某些机型是不适用的,即某些机型上你永远无法达到 60fps(Galaxy core 2 33/60,Nexus 5 55/60,Nexus 4 49/60)。
这个思路牵涉两个核心类/接口:
- Choreographer
- Choreographer$FrameCallback
一次屏幕刷新完成后,将产生 VSync 信号并通知 Choreographer。
Choreographer 收到通知依次处理 Input、Animation、Draw,这三个过程都是通过 FrameCallback 回调的方式完成的。在 Draw 过程中,具体是执行 ViewRootImpl#performTraversals() 方法,完成视图树的 measure、layout、draw 流程。
而 FrameCallback#doFrame(long frameTimeNanos) 方法中可以得到 VSync 到来的时间戳,这样就能得到连续两帧开始渲染之间的间隔,将该值近似作为上一帧的渲染耗时。
实现 FrameCallback 接口,并通过 Choreographer#postFrameCallback() 方法将其跟 Input、Animation、Draw 这些回调一起塞入主线程的消息队列,就能源源不断的获取每一帧的渲染时间戳,每一个 VSync 的时间戳代表一帧,这样可以得到某段时间内渲染完成的帧数,二者相除即可得到帧率。
(上图摘自《Road to 60fps》)
GraphicsBinder
Profile GPU
通过 Profile GPU 可以获得每帧渲染耗时的详细数据,即渲染的每个阶段的耗时情况,方便开发者定位性能瓶颈。
帧渲染耗时柱状图
有两种方式可以查看柱状图:
- 在手机上查看,手机设置—开发者选项— GPU 呈现模式分析(或 GPU 显示配置文件)— 勾选“显示条形图”;
- 在 Android Studio 中查看,打开 GPU 呈现模式分析 — 勾选“在 adb shell dumpsys gfxinfo 中”,柱状图会显示在控制台的 GPU Monitor 区域;
5.0 及以下系统
4.3 系统上效果(在 GPU Monitor 中的效果,绿线表示 16ms,红线表示 33ms):
5.0 上效果(在 GPU Monitor 中的效果):
各个色块所代表的含义及该色块过大的可能原因:
色块 | 阶段 | 含义 |
---|---|---|
Process | 表示 CPU 在等待 GPU 完成渲染的耗时;该阶段耗时大表示 app 在 GPU 中做了过多的操作。 | |
Execute | Android 2d 渲染引擎利用 OpenGL 绘制和刷新 DisplayList 的耗时。该阶段耗时大表示 DisplayList 过多、执行时间过长。 | |
XFer | 上传 bitmap 到 GPU 的耗时。耗时过多表示 app 在加载过多的图形图片。 | |
Update | 创建和更新视图 DisplayList 的耗时。耗时过多可能是由于自定义 view 绘制过多,或者 onDraw() 方法里面操作过多。 |
6.0 及以上系统
在 GPU Monitor 中的效果:
各个色块所代表的含义及该色块过大的可能原因:
色块 | 阶段 | 含义 |
---|---|---|
Swap Buffers | 表示 CPU 在等待 GPU 完成渲染的耗时;该阶段耗时大表示 app 在 GPU 中做了过多的操作。 | |
Command Issue | Android 2d 渲染引擎利用 OpenGL 绘制和刷新 DisplayList 的耗时。该阶段耗时大表示 DisplayList 过多、执行时间过长。 | |
Sync & Upload | 上传 bitmap 到 GPU 的耗时。耗时过多表示 app 在加载过多的图形图片。 | |
Draw | 创建和更新视图 DisplayList 的耗时。耗时过多可能是由于自定义 view 绘制过多,或者 onDraw() 方法里面操作过多。 | |
Measure / Layout | 视图树执行 onMeasure() 和 onLayout() 方法的耗时;耗时过多表示视图树在这两个阶段效率较低。 | |
Animation | 执行动画的耗时。耗时过多可能是因为自定义动画运行效率较低,或者属性刷新出现异常状况。 | |
Input Handling | 执行输入时间回调的耗时。耗时过多可能是因为 app 在处理过多的用户输入时间,可以考虑将这些事件放到其他线程中进行处理。 | |
Misc Time / VSync Delay | 执行连续两帧之间的操作耗时。耗时过多可能是因为 UI 线程操作过多,可以考虑将这些操作放到其他线程中进行处理。 |
在 5.0 上执行 gfxinfo 命令,得到的即为渲染一帧所经过的各个阶段的耗时情况(单位毫秒):
adb shell dumpsys gfxinfo com.demo.app
Draw Prepare Process Execute
0.51 0.69 4.52 0.40
0.43 1.20 3.90 0.36
0.42 0.64 3.70 0.37
0.41 0.68 4.08 0.57
0.46 1.24 3.79 0.35
在 7.0 上执行:
adb shell dumpsys gfxinfo com.demo.app
Stats since: 115689258308387ns
Total frames rendered: 138
Janky frames: 114 (82.61%)
50th percentile: 19ms
90th percentile: 150ms
95th percentile: 200ms
99th percentile: 300ms
Number Missed Vsync: 40
Number High input latency: 2
Number Slow UI thread: 40
Number Slow bitmap uploads: 2
Number Slow issue draw commands: 70
Draw Prepare Process Execute
50.00 0.40 5.48 3.78
50.00 0.77 1.66 3.97
50.00 4.31 2.01 2.59
50.00 5.29 9.59 4.39
50.00 2.95 3.07 8.06
50.00 1.76 1.93 3.12
在 7.0 系统上带上 framestats 参数可以获取最近的 120 帧数据:
adb shell dumpsys gfxinfo com.demo.app framestats
Stats since: 101631537739178ns
Total frames rendered: 42
Janky frames: 31 (73.81%)
50th percentile: 17ms
90th percentile: 19ms
95th percentile: 21ms
99th percentile: 34ms
Number Missed Vsync: 2
Number High input latency: 2
Number Slow UI thread: 3
Number Slow bitmap uploads: 0
Number Slow issue draw commands: 27
---PROFILEDATA---
Flags,IntendedVsync,Vsync,OldestInputEvent,NewestInputEvent,HandleInputStart,AnimationStart,PerformTraversalsStart,DrawStart,SyncQueued,SyncStart,IssueDrawCommandsStart,SwapBuffers,FrameCompleted,
0,101647039145922,101647039145922,101647018084000,101647034145922,101647039815217,101647041206884,101647041424071,101647041635530,101647042167821,101647044205842,101647045030842,101647051882405,101647055263134,
0,101647054735049,101647054735049,101647039692000,101647049735049,101647055237613,101647056265738,101647056492821,101647056665738,101647057101676,101647057342821,101647058124071,101647066840738,101647074210530,
0,101647071403801,101647071403801,101647050345000,101647066403801,101647071899592,101647074218342,101647074530321,101647074697509,101647075244905,101647075473030,101647076719384,101647082193342,101647090448030,
0,101647089118048,101647089118048,101647072068000,101647084118048,101647089415738,101647090049071,101647090219905,101647090331884,101647090610009,101647090709488,101647091358446,101647095696988,101647107083967,
0,101647105786017,101647105786017,101647093579000,101647100786017,101647106096988,101647106731363,101647106896988,101647107007405,101647107266780,101647132519905,101647133169905,101647137525113,101647140328759,
---PROFILEDATA---
第一部分是卡顿的统计数据,包括掉帧率、不同分位值对应的耗时;第二部分(PROFILEDATA)是详细数据,即绘制一帧所经过的各个阶段的起始时间戳,最后一项减去第二项即为该帧的耗时(单位纳秒);
除了 framestats 参数,执行 reset 参数可以清楚帧数据缓存,重新开始记录帧数据。
8.0 上执行得到的数据,跟 7.0 相比在 PROFILEDATA 部分会多处两列:
0,774613991199371,774613991199371,9223372036854775807,0,774613992126069,774613992376069,774613992786486,774613994317736,774613995501069,774613995666173,774613998539611,774614003405757,774614008956798,731000,2023000,
0,774614007852016,774614007852016,9223372036854775807,0,774614008804194,774614009077111,774614009444298,774614010829715,774614011721903,774614011951069,774614015110444,774614019814090,774614023723986,669000,1154000,
0,774614024506735,774614024506735,9223372036854775807,0,774614025356798,774614025628153,774614026001069,774614027616173,774614028856798,774614029033361,774614031576069,774614040898465,774614046081278,2293000,955000,
0,774614207699405,774614207699405,9223372036854775807,0,774614208585444,774614208868778,774614209313048,774614210661486,774614211692736,774614212157319,774614214649507,774614221605757,774614226720861,687000,888000,
0,774614290967221,774614290967221,9223372036854775807,0,774614291604715,774614291821902,774614292169298,774614293051590,774614293895340,774614294029715,774614296510444,774614301460965,774614305854194,468000,1607000,
0,774614407547237,774614407547237,9223372036854775807,0,774614408341694,774614408531277,774614408836486,774614409757840,774614410454715,774614410648986,774614413372423,774614417038048,774614420630236,489000,848000,
0,774614424202018,774614424202018,9223372036854775807,0,774614424797423,774614424998465,774614425294819,774614426081277,774614426981798,774614427118257,774614428749507,774614433750027,774614437724507,415000,932000,
0,774614590744196,774614590744196,9223372036854775807,0,774614591398465,774614591572423,774614591851590,774614592689090,774614593398465,774614593537527,774614595425548,774614600326069,774614604530757,685000,947000,
0,774614607398691,774614607398691,9223372036854775807,0,774614608457840,774614608659402,774614608928152,774614609817215,774614610520861,774614610642736,774614613067215,774614616383361,774614619838569,456000,998000,
0,774614807246899,774614807246899,9223372036854775807,0,774614808425548,774614808673986,774614809040132,774614809862527,774614810560444,774614810714090,774614812768777,774614816448986,774614819731798,562000,821000,
0,774614823900602,774614823900602,9223372036854775807,0,774614824794819,774614824975548,774614825235444,774614825978152,774614826975548,774614827372423,774614829708882,774614834403673,774614837898465,464000,1146000,
0,774614890517842,774614890517842,9223372036854775807,0,774614892127632,774614892273465,774614892565132,774614893267215,774614893978673,774614894082319,774614896123465,774614900918257,774614905893257,499000,1286000,
FrameMetrics
从 7.0(API 24)开始,安卓 SDK 新增 OnFrameMetricsAvailableListener 接口用于提供帧绘制各阶段的耗时,数据源与 GPU Profile 相同。
回调接口为 Window.FrameMetrics:
public interface OnFrameMetricsAvailableListener {
void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics, int dropCountSinceLastInvocation);
}
FrameMetics 存储了如下数据:
阶段 | 含义(纳秒) | 备注 |
---|---|---|
ANIMATION_DURATION | 动画耗时 | |
COMMAND_ISSUE_DURATION | 执行 OpenGL 命令和 DisplayList 耗时 | |
DRAW_DURATION | 创建和更新 DisplayList 耗时 | |
FIRST_DRAW_FRAME | 布尔值,标志该帧是否为此 Window 绘制的第一帧 | 一般忽略此帧 |
INPUT_HANDLING_DURATION | 处理用户输入操作的耗时 | |
INTENDED_VSYNC_TIMESTAMP | 预期 VSync 到来的时间戳 | API >=26 可用 |
LAYOUT_MEASURE_DURATION | layout/measure 耗时 | |
SWAP_BUFFERS_DURATION | CPU 在等待 GPU 完成渲染的耗时 | |
SYNC_DURATION | 上传 bitmap 到 GPU 的耗时 | |
TOTAL_DURATION | 整帧渲染耗时 | |
UNKNOWN_DELAY_DURATION | 未知延迟 | |
VSYNC_TIMESTAMP | VSync 实际到来的时间戳 | API >=26 可用 |
比如使用 ActivityFrameMetrics 的效果:
该方法可以在 Application 统一完成初始化,无需各个页面单独设置。
优势:
1. 官方推荐方式;
2. 能够线上使用;
3. 不限于 120 帧;
4. 能获取第三方应用如竞品应用的帧渲染数据;
劣势:
1. 7.0 及以上系统;
2. 要开启硬件加速;
需要注意的是,虽然 FrameMetrics 与 gfxinfo 的数据同源,但是二者在计算帧率时也有差别。FrameMetrics 获得的是单个帧的数据,正常情况下只能拿到渲染一帧的各个阶段的数据(加起来是该帧总耗时),而不能像 gfxinfo 那样拿到每一帧渲染的起始时间戳。如果用横线表示每帧的渲染,则对开发者而言,FrameMetrics 获得的帧绘制过程是:
—— ———— —— ————
而其实际渲染过程,也即是 gfxinfo 获取的渲染过程可能是:
—— ——
———— ————
分两种情况(具体见 Android5.0中 hwui 中 RenderThread 工作流程 文尾表述):
- 主线程在完成第一帧自己负责的部分后交给 Render Thread 线程,然后自己马不停蹄的继续绘制第二帧;
- 在完成第一帧自己负责的部分后交给 Render Thread 线程,然后等待 Render Thread 完成第一帧剩余的工作后在进行第二帧的绘制;
一次滑动过程中,上述两种情况可能均存在。
故在使用 n/t (以上图为例 n = 4) 计算帧率时,前者的 t 是 n 帧耗时之和(n 帧的渲染是一个串行过程),后者的 t 是最后一帧渲染结束的时间戳减去第一帧渲染开始时的时间戳,前者算出的 t 大于后者算出的 t,得出的帧率也就不一样。原因在于前者未提供获取每一帧渲染起始的时间戳,我们只能以将其作为一个有先后顺序的串行的渲染过程。
另一个需要注意的地方是,log 中的帧并不是连续的,每帧绘制中间会有间隔,较大的间隔。比如我们以 8.0 系统(倒数第三列数据是渲染结束时的时间戳)上获取某次连续滑动得到的帧数据为例:
0,774613991199371,774613991199371,9223372036854775807,0,774613992126069,774613992376069,774613992786486,774613994317736,774613995501069,774613995666173,774613998539611,774614003405757,774614008956798,731000,2023000,
0,774614007852016,774614007852016,9223372036854775807,0,774614008804194,774614009077111,774614009444298,774614010829715,774614011721903,774614011951069,774614015110444,774614019814090,774614023723986,669000,1154000,
0,774614024506735,774614024506735,9223372036854775807,0,774614025356798,774614025628153,774614026001069,774614027616173,774614028856798,774614029033361,774614031576069,774614040898465,774614046081278,2293000,955000,
0,774614207699405,774614207699405,9223372036854775807,0,774614208585444,774614208868778,774614209313048,774614210661486,774614211692736,774614212157319,774614214649507,774614221605757,774614226720861,687000,888000,
0,774614290967221,774614290967221,9223372036854775807,0,774614291604715,774614291821902,774614292169298,774614293051590,774614293895340,774614294029715,774614296510444,774614301460965,774614305854194,468000,1607000,
0,774614407547237,774614407547237,9223372036854775807,0,774614408341694,774614408531277,774614408836486,774614409757840,774614410454715,774614410648986,774614413372423,774614417038048,774614420630236,489000,848000,
0,774614424202018,774614424202018,9223372036854775807,0,774614424797423,774614424998465,774614425294819,774614426081277,774614426981798,774614427118257,774614428749507,774614433750027,774614437724507,415000,932000,
0,774614590744196,774614590744196,9223372036854775807,0,774614591398465,774614591572423,774614591851590,774614592689090,774614593398465,774614593537527,774614595425548,774614600326069,774614604530757,685000,947000,
0,774614607398691,774614607398691,9223372036854775807,0,774614608457840,774614608659402,774614608928152,774614609817215,774614610520861,774614610642736,774614613067215,774614616383361,774614619838569,456000,998000,
0,774614807246899,774614807246899,9223372036854775807,0,774614808425548,774614808673986,774614809040132,774614809862527,774614810560444,774614810714090,774614812768777,774614816448986,774614819731798,562000,821000,
0,774614823900602,774614823900602,9223372036854775807,0,774614824794819,774614824975548,774614825235444,774614825978152,774614826975548,774614827372423,774614829708882,774614834403673,774614837898465,464000,1146000,
0,774614890517842,774614890517842,9223372036854775807,0,774614892127632,774614892273465,774614892565132,774614893267215,774614893978673,774614894082319,774614896123465,774614900918257,774614905893257,499000,1286000,
取每帧的开始和结束时间戳换算成毫秒并将第一帧的开始时间戳作为0,用 highcharts 绘制出甘特图,html 代码和效果图分别如下:
<!DOCTYPE html>
<html lang="en">
<head>
<!--<script src="https://code.highcharts.com/highcharts.js"></script>-->
<!--<script src="https://code.highcharts.com/highcharts-more.js"></script>-->
<!--<script src="https://code.highcharts.com/modules/exporting.js"></script>-->
<!--<script type="text/javascript" src="../../../template/highcharts.js"></script>-->
<!--<script type="text/javascript" src="../../../template/highcharts-more.js"></script>-->
<!--<script type="text/javascript" src="../../../template/exporting.js"></script>-->
<script type="text/javascript" src="jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="highcharts.js"></script>
<script type="text/javascript" src="highcharts-more.js"></script>
<script type="text/javascript" src="exporting.js"></script>
</head>
<body style="margin:0;padding:20px 0 0 0">
<br/>
<center>
<div id="container" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
<div id="frames_log">
0,774613991199371,774613991199371,9223372036854775807,0,774613992126069,774613992376069,774613992786486,774613994317736,774613995501069,774613995666173,774613998539611,774614003405757,774614008956798,731000,2023000,<br>
0,774614007852016,774614007852016,9223372036854775807,0,774614008804194,774614009077111,774614009444298,774614010829715,774614011721903,774614011951069,774614015110444,774614019814090,774614023723986,669000,1154000,<br>
0,774614024506735,774614024506735,9223372036854775807,0,774614025356798,774614025628153,774614026001069,774614027616173,774614028856798,774614029033361,774614031576069,774614040898465,774614046081278,2293000,955000,<br>
0,774614207699405,774614207699405,9223372036854775807,0,774614208585444,774614208868778,774614209313048,774614210661486,774614211692736,774614212157319,774614214649507,774614221605757,774614226720861,687000,888000,<br>
0,774614290967221,774614290967221,9223372036854775807,0,774614291604715,774614291821902,774614292169298,774614293051590,774614293895340,774614294029715,774614296510444,774614301460965,774614305854194,468000,1607000,<br>
0,774614407547237,774614407547237,9223372036854775807,0,774614408341694,774614408531277,774614408836486,774614409757840,774614410454715,774614410648986,774614413372423,774614417038048,774614420630236,489000,848000,<br>
0,774614424202018,774614424202018,9223372036854775807,0,774614424797423,774614424998465,774614425294819,774614426081277,774614426981798,774614427118257,774614428749507,774614433750027,774614437724507,415000,932000,<br>
0,774614590744196,774614590744196,9223372036854775807,0,774614591398465,774614591572423,774614591851590,774614592689090,774614593398465,774614593537527,774614595425548,774614600326069,774614604530757,685000,947000,<br>
0,774614607398691,774614607398691,9223372036854775807,0,774614608457840,774614608659402,774614608928152,774614609817215,774614610520861,774614610642736,774614613067215,774614616383361,774614619838569,456000,998000,<br>
0,774614807246899,774614807246899,9223372036854775807,0,774614808425548,774614808673986,774614809040132,774614809862527,774614810560444,774614810714090,774614812768777,774614816448986,774614819731798,562000,821000,<br>
0,774614823900602,774614823900602,9223372036854775807,0,774614824794819,774614824975548,774614825235444,774614825978152,774614826975548,774614827372423,774614829708882,774614834403673,774614837898465,464000,1146000,<br>
0,774614890517842,774614890517842,9223372036854775807,0,774614892127632,774614892273465,774614892565132,774614893267215,774614893978673,774614894082319,774614896123465,774614900918257,774614905893257,499000,1286000,<br>
</div>
</center>
<script>
var lines = $("#frames_log").html().trim().split('\n')
var startIndex = 1
var endIndex = 13
var baseStamp
var logs = []
for (var i = 0; i < lines.length; i++) {
var line = lines[i]
var stamps = line.split(',')
var pair = []
pair[0] = stamps[startIndex]
pair[1] = stamps[endIndex]
if (i == 0) {
baseStamp = pair[0]
}
logs[i] = pair
}
for (var i = 0; i < logs.length; i++) {
pair = logs[i]
for (var j = 0; j < pair.length; j++) {
pair[j] = (pair[j] - baseStamp) / 1000000
}
console.log(pair)
}
Highcharts.chart('container', {
chart: {
type: 'columnrange',
inverted: true
},
title: {
text: 'frame rendering gant'
},
subtitle: {
text: 'from 8.0'
},
xAxis: {
categories: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']
},
scrollbar: {
enabled: true
},
yAxis: {
title: {
text: 'ms'
}
},
tooltip: {
valueSuffix: ''
},
plotOptions: {
columnrange: {
dataLabels: {
enabled: true,
formatter: function () {
return "";
}
}
}
},
legend: {
enabled: false
},
series: [{
name: 'cost',
data: logs
}]
});
</script>
</body>
</html>
可以看出,其中有多处超过 100ms 的间隙。
更进一步,我们知道只要耗时大于 16.6 ms,就会发生掉帧,按正常逻辑来说,掉帧越多,帧率会越低。考虑下图这种极端情况:
渲染了 A B C D 共 4 帧,每帧都耗时 2 个 VSync 周期,即每帧都出现了掉帧,掉帧率 100%,但是由于 UI 线程和 Render 线程并行进行,5 个 VSync 周期渲染完成了 4 帧。如果不只是 4 帧,而是 n 帧,n 较大,那么可以认为 n 个 VSync 周期渲染完成了 n 帧。帧率就是 60,但是掉帧率是 100%。这显然与“掉帧越多,帧率会越低”是矛盾的,甚至是违反常识的。所以,我认为,在 Render 线程存在的情况(即开启硬件加速的情况下),帧率已经无法作为表征 UI 性能的指标了。这也可以从侧面解释,为什么 gfxInfo 也好,Google Vitals 也好,都给出了帧渲染数据、掉帧率等,但是就是没给出帧率这个至关重要的数据指标。当然这只是一己之见,欢迎讨论指正。
通过 onDraw() 获取帧数据
可能有些同学会心生疑问:就这这两种吗?重写自定义 View 的 onDraw() 方法,在里面加上时间戳,我们也能把 onDraw() 的调用次数当做渲染完成帧数,首尾相减作为耗时,分子和分母都有了,不是也能得到帧率吗?
我们通过一张图来对比下这种方法和上述两种方式的差异和合理性。
上图是通过 systrace 在华为 mate9 pro (7.0 系统)真机上获取的。从 5.0 系统开始,安卓系统使用 UI 线程 + Render 线程共同完成帧渲染。该帧圆圈被标志为黄色,表示渲染耗时超过 16.6ms。
对该帧而言,通过 Choreographer 和 gfxinfo 获取的帧渲染耗时是不相等的。Choreographer 方式得到的帧耗时依然是 16.6ms,而 gfxinfo 则大于 16.6ms。
注意,图中竖直方向上的 UI 线程和 Render 线程的色块耗时并不总是对应的。可能由于掉帧现象的出现,导致 UI 线程绘制的帧对应的 Render 线程绘制过程后移。
而对于,蓝色部分,由于我们无法准确控制自定义 View onDraw() 方法开始执行的时间,得到的耗时是非常不准确的,而且无法排除这个耗时包括了滑动停顿的时间,更加加大了这种方式的误差。
UI 线程和 Render 线程共同完成帧绘制和渲染,也就是说,一帧的耗时分布在两个线程中,等于二者的耗时之和。但是 Choreographer 方式只统计了 UI 线程中的部分,而忽略了 Render 线程的部分,这样会导致得到的耗时偏小、帧率偏大。更进一步,既然一帧的绘制和渲染分布于两个线程, 那即使一帧的耗时综合大于 16.6 ms,只要位于 UI 线程部分的耗时不超过 16.6ms,那么就不影响 UI 线程绘制下一帧,但是会导致屏幕接着显示上一帧的内容,出现掉帧,而 Choreographer 仅凭帧耗时无法检测出这种掉帧。
而且,从可行性上考虑,重写 onDraw() 获取帧数据的方式并不靠谱。因为:
- 对业务代码侵入性太强;
- 自定义 View 应该放在视图树的哪个层级呢?叶节点?
性能指标
关于帧率和掉帧率,这里注重阐释一下。通过这个视频 why 60 fps ?,我们可以看出,30 fps 和 60 fps 的区别并不是那么的明显,如果每一个瞬间页面的帧率都是 30 fps 其实是感觉不到卡顿的,只是会感觉到滞后感、拖拉感,放佛活在慢镜头里。而“卡顿”,出现在帧率突变的时刻,比如前一段时间都是 60fps 突然降到 30 fps 了,就能明显感觉卡顿。
帧率可以衡量一个时间段内的的渲染性能,但是比较粗略。比如,在相同的时间内,掉了 500 帧,下面两种情况的帧率相同,但是用户体验却天壤之别:
- 每两帧掉一帧,即掉帧均匀分布,每帧的渲染耗时均在 17-32 ms,此时用户感受到相对流畅的页面滑动;
- 掉帧不均匀,掉帧集中出现在某段时间内,那么在这段时间内用户会觉得“ app 卡死了,界面冻住了”,估计多数用户此时会杀掉 app;
因此,单纯通过帧率来衡量性能是不够严谨的,比如 Facebook 就用连续掉 2+ 帧的比例来衡量 fps,Jason Sendros 对此指标合理性的解释是( 视频《Road to 60fps》第 14:14 处):
1 frame drop is noticeable if you are staring at something and the rest of the app is buttery smooth. If you start where you are not super buttery smooth 1 frame drop is going to completely unnoticeable.
2 consecutive frame drops is a little bit noticeable and it’s kind of annoying.
3 frame drops gets a bit worse.
4 gets a little irritating.
By 5 frame drops you are not even sure if the app is responding to you when you are doing something for a short period of time and it’s just a really frustrating experience.
除了帧率,还有如下指标用于衡量页面滑动的流畅程度:
- 掉帧率,其实更确切说是“超时率”,即耗时超过 16.6 ms 的帧在所有渲染完成的帧总数的占比。某一帧“掉了”,并不是这一帧没有绘制完成被丢掉了或者没被显示在屏幕上,而是,虽然没有“按时”显示在屏幕上但是还是显示了。帧率无法衡量掉帧的分布程度,是密集还是分散。
- 出现连续 2+ 掉帧的比例(Facebook) 遭遇掉帧率在 50%+ 的用户的比例(Slow Rendering,Engineer for High Performance with Tools from Android & Play 25:48)
- 遭遇 700ms+ 耗时帧占比大于 0.1% 的用户的比例(Frozen Frames,Engineer for High Performance with Tools from Android & Play 25:48)
更多好文
- Android Performance: An Overview (Google I/O ‘17)
- Android5.0中 hwui 中 RenderThread 工作流程
- Road to 60fps
- GPU Monitor
- Analyzing with Profile GPU Rendering
- Profile GPU Rendering Walkthrough
- Testing UI Performance
- Graphics architecture
- 关于 android 通过 python 统计 fps
- Speed up your app
- Testing Android UI Performance
- android-perf-testing
- dumpsys 实现原理
- 手机性能评测–2D场景
- Engineer for High Performance with Tools from Android & Play
- why 60 fps?
推荐阅读