首先解释一下题目中的“帧渲染数据”。
“帧渲染数据”是指,完成渲染一帧的耗时。
如果我们拿到了每一帧的耗时,我们就拿到了两个数据:某段连续时间 deltT 内渲染完成的帧数 n,那么 deltT / n 就是我们常说的帧率。
安卓原生系统没有直接提供帧率这个性能指标数据(魅族 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.dianping.v1
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.dianping.v1 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 参数可以清楚帧数据缓存,重新开始记录帧数据。
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 帧;
劣势:
1. 7.0 及以上系统;
2. 要开启硬件加速;
性能指标
帧率可以衡量一个时间段内的的渲染性能,但是比较粗略。比如,在相同的时间内,掉了 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.
除了帧率,还有如下指标用于衡量页面滑动的流畅程度:
- 掉帧率
- 出现连续 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)
更多好文
- 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