文章目录
首先解释一下文章标题中的“帧渲染数据”。
“帧渲染数据”是指,完成渲染一帧的耗时。这是计算帧率的基础数据。
截止到 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 的时间戳代表一帧,这样可以得到某段时间内渲染完成的帧数,二者相除即可得到帧率。
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 在处理过多的用户输入时间,可以考虑将这些事件放到其他线程中 |