为了更好的理解使用
Choreographer
监控App FPS
的原理,本文先来梳理一下Choreographer
的工作原理。
Choreographer
主要是用来协调动画、输入和绘制事件运行的。它通过接收Vsync
信号来调度应用下一帧渲染时的动作。
对 Vsync 信号的监听
Choreographer
会通过Choreographer.FrameDisplayEventReceiver
来监听底层HWC
触发的Vsync
信号:
private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
}
}
复制代码
Vsync
信号可以理解为底层硬件的一个系统中断,它每16ms会产生一次。上面onVsync()
的每个参数的意义为:
timestampNanos : Vsync信号到来的时间, 这个时间使用的是底层
JVM nanoscends -> System.nanoTime
builtInDisplayId : 此时
SurfaceFlinger
内置的display id
frame : 帧号,随着
onVsync
的回调数增加
onVsync的回调逻辑
那onVsync
什么时候会调用呢?
Choreographer.scheduleVsyncLocked()
会请求下一次Vsync
信号到来时回调FrameDisplayEventReceiver.onVsync()
方法:
private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}
复制代码
Vsync信号到来时执行callback
设置callback
Choregrapher
提供了下面方法设置callback
:
public void postCallback(int callbackType, Runnable action, Object token)
public void postCallbackDelayed(int callbackType, Runnable action, Object token, long delayMillis)
public void postFrameCallback(FrameCallback callback)
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis)
复制代码
在Choregrapher
中存在多个Callback Queue
, 常见的Callback Queue
的类型有:
Choreographer.CALLBACK_INPUT 输入事件,比如键盘
Choreographer.CALLBACK_ANIMATION 动画
Choreographer.CALLBACK_TRAVERSAL 比如`ViewRootImpl.scheduleTraversals, layout or draw`
Choreographer.CALLBACK_COMMIT
复制代码
上面4个事件会在一次Vsync
信号到来时依次执行。
callback的执行逻辑
以ViewRootImpl.scheduleTraversals
为例:
void scheduleTraversals() {
...
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
复制代码
即给Choregrapher
提交了一个Choreographer.CALLBACK_TRAVERSAL
类型的callback
去执行。
postCallback()
里面的具体执行逻辑就不分析了,这里直接说一下关键逻辑:
- 切换到
Choregrapher
创建时所在的线程去调用scheduleFrameLocked()
方法,设置mFrameScheduled = true
- 调用
scheduleVsyncLocked
请求下一次Vsync
信号回调 FrameDisplayEventReceiver.onVsync()
会生成一个消息,然后发送到Choreographer.mHander
的消息队列Choreographer.mHander
取出上面onVsync
中发送的消息,执行Choreographer.doFrame()
方法,doFrame()
中判断mFrameScheduled
是否为true
,如果为true
的话就上面四种callback
综上所述Choreographer
的工作原理如下图:
doFrame() 的时间参数
我们来看一下这个方法(主要关注一下时间参数):
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
if (!mFrameScheduled) {
return; // no work to do
}
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) { //16ms
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
frameTimeNanos = startNanos - lastFrameOffset;
}
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
mFrameScheduled = false;
mLastFrameTimeNanos = frameTimeNanos;
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
}
复制代码
解释一下上面一些时间相关参数的含义:
intendedFrameTimeNanos: 预计这一帧开始渲染的时间
frameTimeNanos: 这一帧真正开始渲染的时间。在
startNanos - frameTimeNanos < mFrameIntervalNanos
,其实就等于intendedFrameTimeNanos
jitterNanos: 真正渲染时间点和预计渲染时间点之差
mFrameIntervalNanos: 每一帧期望渲染的时间, 固定为16ms
skippedFrames : jitterNanos总共跳过了多少帧。
mLastFrameTimeNanos : 上一次渲染一帧的时间点
那 jitterNanos > mFrameIntervalNanos
在什么时候会成立呢?
其实就是我们常说的丢帧: 比如我们连续提交了两个Choreographer.CALLBACK_TRAVERSAL callback
。如果一个callback
的执行时间大于16ms,那么就会造成:
startNanos - frameTimeNanos > mFrameIntervalNanos(16ms)
复制代码
doCallback(int callbackType, long frameTimeNanos)
这个方法的逻辑并不复杂 : 获取callbackType
对应的Callback Queue
, 取出这个队列中已经过期的calllback
进行执行。
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now / TimeUtils.NANOS_PER_MS);
for (CallbackRecord c = callbacks; c != null; c = c.next) {
if (DEBUG_FRAMES) {
Log.d(TAG, "RunCallback: type=" + callbackType
+ ", action=" + c.action + ", token=" + c.token
+ ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
}
c.run(frameTimeNanos);
}
}
复制代码
Choreographer与主线程消息循环的关系
上面我们已经知道onVsync
把要执行doFrame
的消息放入了Choreographer.mHander
的消息队列。
这里Choreographer.mHander的消息队列其实就是主线程的消息,所以doFrame方法其实是由主线程的消息循环来调度的。
我们看一下Choreographer
实例化时的Looper
:
private static final ThreadLocal<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
Looper looper = Looper.myLooper();
...
Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
if (looper == Looper.getMainLooper()) {
mMainInstance = choreographer;
}
return choreographer;
}
};
复制代码
即取的是当前线程的Looper
,所以donFrame()
是在主线程的消息循环中调度的。
参考文章:
Android Choreographer 源码
后面会分析Tencent/matrix的实现原理,欢迎关注我的Android进阶计划看更多文章。