Android FPS 计算
简述绘制过程
简单描述一下 View 的绘制过程:SurfaceFlinger
在收到 Vsync
脉冲信号后会,通过 IPC 的通信方式通知位于应用层的 Choreographer
, Choreographer
在收到信号后会通过 Handler
把 Vsync 同步信号发送至主线程,这时候 Choregrapher
会根据目前的状态,选择下发触摸事件,动画,绘制(包括 layout,measure,draw) 等等。这些事件会下发至 ViewRootImpl
依次在下发至 Activity
, View
等我们应用程常见的控件,如果是绘制的事件,最终会完成整个 View 树的 layout,measure,draw 过程(这整个过程都是由 CPU 在主线程上完成),主线程完成后会把绘制的数据传递给应用层的RenderThread
(开启硬件加速),RenderThread
会向 SurfaceFlinger
请求获取绘制数据的 Buffer,这个过程也是通过 Binder
完成的,获取到 Buffer 后将主线程绘制的数据通过 GPU 绘制(通常是通过 OpenGL ES)到 Buffer 上,绘制完成后 RenderThread
通过 Binder
将填充好数据的 Buffer 传递给 SurfaceFlinger
,SurfaceFlinger
最后合成这些数据再传递给 HardwareComposer
(HAL 层),完成一帧画面的显示。
Vsync 信号中的重要信息
我们从应用层 Choregorapher
入手,它是通过他的私有内部类 FrameDisplayEventReceiver
获取 Vsync
同步信号,它是继承至 DisplayEventReceiver
,回调方法是 onVsync()
方法:
/**
* Called when a vertical sync pulse is received.
* The recipient should render a frame and then call {@link #scheduleVsync}
* to schedule the next vertical sync pulse.
*
* @param timestampNanos The timestamp of the pulse, in the {@link System#nanoTime()}
* timebase.
* @param physicalDisplayId Stable display ID that uniquely describes a (display, port) pair.
* @param frame The frame number. Increases by one for each vertical sync interval.
* @param vsyncEventData The vsync event data.
*/
public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
VsyncEventData vsyncEventData) {
}
其中比较重要的参数:
-
timestampNanos
Vsync 发生的的时间戳 -
frame
frame 的编号,是依次增长的 -
vsyncEventData
见以下面的代码
static final class VsyncEventData {
// The frame timeline vsync id, used to correlate a frame
// produced by HWUI with the timeline data stored in Surface Flinger.
public final long id;
// The frame deadline timestamp in {@link System#nanoTime()} timebase that it is
// allotted for the frame to be completed.
public final long frameDeadline;
/**
* The current interval between frames in ns. This will be used to align
* {@link FrameInfo#VSYNC} to the current vsync in case Choreographer callback wa
* delayed by the app.
*/
public final long frameInterval;
VsyncEventData(long id, long frameDeadline, long frameInterval) {
this.id = id;
this.frameDeadline = frameDeadline;
this.frameInterval = frameInterval;
}
VsyncEventData() {
this.id = FrameInfo.INVALID_VSYNC_ID;
this.frameDeadline = Long.MAX_VALUE;
this.frameInterval = -1;
}
}
VsyncEventData 中比较重要的参数:
-
frameDeadline
表示当前帧要在该时间之前完成绘制,如果超过该时间就有可能导致丢帧(因为有三重缓存的存在,所以不是一定会掉帧)。 -
frameInterval
帧之间的间隔,屏幕刷新率 60 Hz 为例,间隔就为1 / 60 * 10 ^ 9
纳秒。
通过上面的 Vsync 同步参数我们就可以很容易地计算出 FPS,或者当前的帧绘制是否超过了间隔。
Android 源码中掉帧计算
没错,在 Choregorapher
源码中有掉帧的计算,我们先从它收到 Vsync 信号开始看:
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;
private VsyncEventData mLastVsyncEventData = new VsyncEventData();
public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource, 0);
}
// TODO(b/116025192): physicalDisplayId is ignored because SF only emits VSYNC events for
// the internal display and DisplayEventReceiver#scheduleVsync only allows requesting VSYNC
// for the internal display implicitly.
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
VsyncEventData vsyncEventData) {
try {
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW,
"Choreographer#onVsync " + vsyncEventData.id);
}
// Post the vsync event to the Handler.
// The idea is to prevent incoming vsync events from completely starving
// the message queue. If there are no messages in the queue with timestamps
// earlier than the frame time, then the vsync event will be processed immediately.
// Otherwise, messages that predate the vsync event will be handled first.
long now = System.nanoTime();
if (timestampNanos > now) {
Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
+ " ms in the future! Check that graphics HAL is generating vsync "
+ "timestamps using the correct timebase.");
timestampNanos = now;
}
if (mHavePendingVsync) {
Log.w(TAG, "Already have a pending vsync event. There should only be "
+ "one at a time.");
} else {
mHavePendingVsync = true;
}
mTimestampNanos = timestampNanos;
mFrame = frame;
mLastVsyncEventData = vsyncEventData;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
}
}
上面源码很简单,在 onVsync
方法回调中(工作在 Binder 线程),通过 handler 加入消息队列 (注意这里通过 setAsynchronous()
方法将该消息设置为异步消息,会优先被执行),这个 handler 的 Looper
是主线程的,也就是会在主线程执行,最后会执行 run()
方法,后进入 doFrame()
方法(这里还通过 mHavePendingVsync
变量来判断是否有消息还在队列里面没有执行,如果没有执行,就跳过,也就是出现了掉帧)。
在 doFrame()
方法中有一段代码来计算掉帧,这段代码是在绘制等事件下发前,可以理解为,前面的帧绘制时间过长导致的掉帧:
// ..
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= frameIntervalNanos) {
final long skippedFrames = jitterNanos / frameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
final long lastFrameOffset = jitterNanos % frameIntervalNanos;
if (DEBUG_JANK) {
Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
+ "which is more than the frame interval of "
+ (frameIntervalNanos * 0.000001f) + " ms! "
+ "Skipping " + skippedFrames + " frames and setting frame "
+ "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
}
frameTimeNanos = startNanos - lastFrameOffset;
}
// ..
通过当前时间减去 Vsync 脉冲产生的时间(startNanos - frameTimeNanos
)来表示之间间隔(jitterNanos
),因为这个时间还没有做耗时的类似于绘制的操作,如果正常的情况下,肯定是远远小于正常的帧间隔的(frameIntervalNanos
),如果是大于这个值了(也可以是当前时间肯定大于了 frameDeadline
),肯定是前面的帧绘制耗时时间过长导致的(因为这是一个消息队列,前面的帧绘制时间过长,就会导致后续的任务延迟执行),所以就可以通过 jitterNanos / frameIntervalNanos
来计算出掉了几帧。
简单的 FPS 计算
在应用层想要计算肯定涉及到反射,在 Android 中如果绕过高版本的反射限制,可以在网上找找方法。这里给一个简单的计算方式,在每次的 Vsync 绘制完成是来计算,通过当前的时间减去 timestampNanos
表示当前帧的绘制耗时,这个耗时如果小于 frameInterval
表示没有出现掉帧,当前的帧率可以近似认为等于当前刷新率,计算方法为 1 * 10 ^ 9 / frameInterval
; 如果绘制耗时大于 frameInterval
就表示掉帧了(并不一定掉帧,因为有三重缓存的存在,只是简单暴力认为掉帧了),当前的 FPS 计算方法为 1 * 10 ^ 9 / (System.nanoTime() - timestampNanos)
。
这个 FPS 的计算不是绝对的准确,因为没有考虑到三重缓存、RenderThread
和 SurfaceFlinger
的耗时;如果像 Android 的实现一样,以收到 Vsync 后为基准时间,来计算上一次绘制的 FPS,要准一些,就有一帧的延迟。具体怎么计算那就取决于你自己了,实践是最好的验证方法,我这里只是简单贴出计算方法,我觉得你应该有更好的方法。
网友评论