[Framework] Android Choreographer 工作原理

Choreographer

简介

Choreographer 通过内部的 FrameDisplayEventReceiver 来接收 Vsync(是由 SurfaceFlinger 下发的) 信号,接收到 Vsync 后统一处理 InputEvent (部分事件由这里处理,下发至 ViewRootImpl)、Animation (如果是调用的 View#postOnAnimation 方法,就会直接回调给对应的 Runnable) 和 Traversal (也就是对应的 View 的 layout, measure 和 draw 流程,事件是下发至 ViewRootImpl,然后通过冒泡的方式依次下发至所有的 View)等任务。

这里以 View 的绘制为例子来描述一下这个过程:假如 View 的 UI 需要修改,我们都会调用 View#invalidate() 方法请求重新绘制,这个方法会异常调用 parent 的 invalidate 方法,最后会调用到 ViewRootImpl 中的 invalidate 方法,在 ViewRootImpl 中会直接向 Choreographer 中请求添加一个 Traversal 类型的任务,在下次的 Vsync 信号来了后就会执行这个任务(如果这时还有别的任务也会执行),最后完成绘制。

在 Activity 进行 resume 的生命周期时会创建 ViewRootImpl 实例 (别的和 UI 相关的地方也有可能创建,例如 Dialog.),ViewRootImpl 的构造函数中会去获取 Choreographer 实例,这是一个单例对象。

后续的代码阅读都是基于 API 31

获取 Vsync

在 Choreographer 的构造函数中会初始化 FrameDisplayEventReceiver 实例,来用于获取 Vsync 信号,其中 onVsync 方法就是垂直同步的信号回调,其中该方法中包含了帧的一些重要信息,包括 Vsync 信号的时间戳、帧序号、帧间隔、帧绘制完成的 Deadline:

Java 复制代码
    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 回调方法中首先检查信号的时间戳如果大于当前的时间丢弃当前信号,如果上次的 Vsync 信号还没有处理完也直接丢弃。然后通过 handler (在初始化的时候创建,工作在主线程) 将其加入到任务队列中,注意这里的 msg 是异步的(处理优先级更高),最后任务的处理是在 run 方法中,处理的入口函数是 doFrame 方法,这里还有一点要提醒一下 onVsync 信号回调不是每次都会调用的,需要手动调用 FrameDisplayEventReceiver#scheduleVsync() 方法后才会把下次的 Vsync 信号回调给我们。

执行任务

在讲执行任务之前,我们先讲一讲这些等待执行的任务是放在哪里的,怎么放的。他们都是按照分类放在 private final CallbackQueue[] mCallbackQueues; 中,这个数组中依次放了 InputEvent(CALLBACK_INPUT), Animation(CALLBACK_ANIMATION 和 CALLBACK_INSETS_ANIMATION), Traversal(CALLBACK_TRAVERSAL) 的任务队列。CallbackQueue 中的实现是链表,执行完的任务就会被移除链表,他们的具体代码我就不贴了,感兴趣的自己去看一下。

接下来看看上面提到的任务处理的入口函数 doFrame。

在开始任务前,会通过当前的时间与 Vsync 产生的时间作差然后和帧间隔相比,如果大于帧间隔就表示前面帧处理的时间过长,导致了掉帧:

Java 复制代码
    void doFrame(long frameTimeNanos, int frame,
                 DisplayEventReceiver.VsyncEventData vsyncEventData) {
        // ...
        long intendedFrameTimeNanos = frameTimeNanos;
        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;
        }

        //...
    }

上面的代码块计算了掉帧,根据掉帧的 offset 重新计算了帧时间。

下面的代码添加了两个跳过 frame 绘制的逻辑,跳过本次绘制后会通过 scheduleVsyncLocked() 方法在再次请求下次的 Vsync 信号。

Java 复制代码
    void doFrame(long frameTimeNanos, int frame,
                 DisplayEventReceiver.VsyncEventData vsyncEventData) {
        // ...
        if (frameTimeNanos < mLastFrameTimeNanos) {
            if (DEBUG_JANK) {
                Log.d(TAG, "Frame time appears to be going backwards.  May be due to a "
                        + "previously skipped frame.  Waiting for next vsync.");
            }
            traceMessage("Frame time goes backward");
            scheduleVsyncLocked();
            return;
        }
        if (mFPSDivisor > 1) {
            long timeSinceVsync = frameTimeNanos - mLastFrameTimeNanos;
            if (timeSinceVsync < (frameIntervalNanos * mFPSDivisor) && timeSinceVsync > 0) {
                traceMessage("Frame skipped due to FPSDivisor");
                scheduleVsyncLocked();
                return;
            }
        }

        //...
    }

如果当前的帧时间戳逼上次的帧时间戳还小的时候跳过本次绘制(通常是前面的帧绘制超时导致的);还有就是通过 mFPSDivisor 来限制刷新时间间隔,这样可以控制帧率,通常这个值是 1,也就是不限制。

以下就进入任务执行了:

Java 复制代码
    void doFrame(long frameTimeNanos, int frame,
                 DisplayEventReceiver.VsyncEventData vsyncEventData) {
        // ...
        mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos, vsyncEventData.id,
                vsyncEventData.frameDeadline, startNanos, vsyncEventData.frameInterval);
        AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
        mFrameInfo.markInputHandlingStart();
        doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos, frameIntervalNanos);
        mFrameInfo.markAnimationsStart();
        doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos, frameIntervalNanos);
        doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos,
                frameIntervalNanos);
        mFrameInfo.markPerformTraversalsStart();
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos, frameIntervalNanos);
        doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos, frameIntervalNanos);

        //...
    }

其中 mFrameInfo 中记录了很多有用的信息,包括 Vsync 的开始时间、帧间隔、绘制完成的deadline、每个阶段的耗时等信息,在应用性能分析时这些数据很有用。

任务按 CALLBACK_INPUT, CALLBACK_ANIMATION, CALLBACK_INSETS_ANIMATION, CALLBACK_TRAVERSAL 依次执行,最后的实际任务执行都调用了 doCallbacks 方法:

Java 复制代码
    void doCallbacks(int callbackType, long frameTimeNanos, long frameIntervalNanos) {
        CallbackRecord callbacks;
        synchronized (mLock) {
            // We use "now" to determine when callbacks become due because it's possible
            // for earlier processing phases in a frame to post callbacks that should run
            // in a following phase, such as an input event that causes an animation to start.
            final long now = System.nanoTime();
            callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                    now / TimeUtils.NANOS_PER_MS);
            if (callbacks == null) {
                return;
            }
            mCallbacksRunning = true;

            // Update the frame time if necessary when committing the frame.
            // We only update the frame time if we are more than 2 frames late reaching
            // the commit phase.  This ensures that the frame time which is observed by the
            // callbacks will always increase from one frame to the next and never repeat.
            // We never want the next frame's starting frame time to end up being less than
            // or equal to the previous frame's commit frame time.  Keep in mind that the
            // next frame has most likely already been scheduled by now so we play it
            // safe by ensuring the commit time is always at least one frame behind.
            if (callbackType == Choreographer.CALLBACK_COMMIT) {
                final long jitterNanos = now - frameTimeNanos;
                Trace.traceCounter(Trace.TRACE_TAG_VIEW, "jitterNanos", (int) jitterNanos);
                if (jitterNanos >= 2 * frameIntervalNanos) {
                    final long lastFrameOffset = jitterNanos % frameIntervalNanos
                            + frameIntervalNanos;
                    if (DEBUG_JANK) {
                        Log.d(TAG, "Commit callback delayed by " + (jitterNanos * 0.000001f)
                                + " ms which is more than twice the frame interval of "
                                + (frameIntervalNanos * 0.000001f) + " ms!  "
                                + "Setting frame time to " + (lastFrameOffset * 0.000001f)
                                + " ms in the past.");
                        mDebugPrintNextFrameTimeDelta = true;
                    }
                    frameTimeNanos = now - lastFrameOffset;
                    mLastFrameTimeNanos = frameTimeNanos;
                }
            }
        }
        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
            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);
            }
        } finally {
            synchronized (mLock) {
                mCallbacksRunning = false;
                do {
                    final CallbackRecord next = callbacks.next;
                    recycleCallbackLocked(callbacks);
                    callbacks = next;
                } while (callbacks != null);
            }
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

首先通过需要执行的类型从 mCallbackQueues 中拿到想要的任务队列,然后根据时间把过期的 callback 过滤掉,如果是 CALLBACK_COMMIT(Vsync 任务的最后一种类型) 还会记录一些超时的日志和更新一些状态相关的变量。然后依次执行任务,Callback 对应的类是 CallbackRecord,执行时调用对应的 run 方法。最后回收这些 Callback。

Java 复制代码
private static final class CallbackRecord {
    public CallbackRecord next;
    public long dueTime;
    public Object action; // Runnable or FrameCallback
    public Object token;
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public void run(long frameTimeNanos) {
        if (token == FRAME_CALLBACK_TOKEN) {
            ((FrameCallback)action).doFrame(frameTimeNanos);
        } else {
            ((Runnable)action).run();
        }
    }
}

CallbackRecord 中的代码真的非常简单,就不多说了.

再看看 View 的请求绘制和动画

View 的请求绘制

众所周知如果需要更新 View 的 UI,需要调用 View#invalidate() 方法。最后几经跳转会执行下面的代码:

Java 复制代码
// ...
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
    final Rect damage = ai.mTmpInvalRect;
    damage.set(l, t, r, b);
    p.invalidateChild(this, damage);
}
// ...

他会调用 parent 的 invalidateChild 方法,我们继续跟进 ViewGroup 的 invalidateChild 方法,发现它也会调用它的 parent 的 invalidateChild 方法,也就是说它会一层一层往上调用,直到顶层的 parent,这里我直接说结论,顶层的 parent 就是 ViewRootImpl。

在 ViewRootImpl#invalidateChild 方法中最后会兜兜转转调用到 scheduleTraversal 方法中。

Java 复制代码
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

经过层层调用,最终终于看到了我们熟悉的 Choreographer,他在主线程的 Looper 队列中加了一个屏障(提高任务的优先级), 然后将 mTraversalRunnable 添加到 CALLBACK_TRAVERSAL 队列中,等待下次 Vsync 信号来的时候执行。

Java 复制代码
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

TraversalRunnable 直接在 run 中调用 doTraversal 方法,这个方法就是触发整个 ViewTree measure、layout 和 draw 的方法。

在 Choreographer#postCallback 方法中最后会调用到 postCallbackDelayedInternal 方法中:

Java 复制代码
 private void postCallbackDelayedInternal(int callbackType,
         Object action, Object token, long delayMillis) {
     if (DEBUG_FRAMES) {
         Log.d(TAG, "PostCallback: type=" + callbackType
                 + ", action=" + action + ", token=" + token
                 + ", delayMillis=" + delayMillis);
     }
     synchronized (mLock) {
         final long now = SystemClock.uptimeMillis();
         final long dueTime = now + delayMillis;
         mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
         if (dueTime <= now) {
             scheduleFrameLocked(now);
         } else {
             Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action
             msg.arg1 = callbackType;
             msg.setAsynchronous(true);
             mHandler.sendMessageAtTime(msg, dueTime);
         }
     }
 }

首先将任务添加到对应 callbackType 的队列中,如果请求的时间小于等于当前时间,直接请求下次的 Vsync 信号;如果还未到执行的时间,那就通过 handler 发一个延迟任务,时间到了再请求 Vsync 信号.

View 动画

我们直接看 View#postOnAnimation 方法:

Java 复制代码
public void postOnAnimation(Runnable action) {
   final AttachInfo attachInfo = mAttachInfo;
   if (attachInfo != null) {
       attachInfo.mViewRootImpl.mChoreographer.postCallback(
               Choreographer.CALLBACK_ANIMATION, action, null);
   } else {
       // Postpone the runnable until we know
       // on which thread it needs to run.
       getRunQueue().post(action);
   }
}

一点弯子都不绕,直接向 Choreographer 中直接添加 CALLBACK_ANIMATION 任务.

相关推荐
小锋学长生活大爆炸4 分钟前
【工具】在 Android 手机上运行 Docker
android·docker·智能手机
Carson带你学Android14 分钟前
OpenAI Dots 登陆手机端:手机上派任务,AI 在云端写代码
android·ios
事圆则缓19 分钟前
Android 依赖注入库到底应该怎么选
android
大圣编蚕10 小时前
初探 NDK 的世界:从零开始理解 Android 原生开发
android
云贝贝贝10 小时前
MySQL 慢查询定位与索引优化实战
android
骑着蜗牛撵大象32711 小时前
服务下线不再炸:分层关闭、TCP 存活探测与负载均衡排水的落地套路
android·tcp/ip·负载均衡·tcp·高可用·健康检查·优雅关闭
三少爷的鞋13 小时前
别再这么写协程了!Marcin Moskała 剖析的 几个常见协程误区与重构
android
RobinXYuan14 小时前
【LevelDB 源码阅读】05 | WriteBatch:原子写的载体
源码阅读·leveldb
mmsx14 小时前
Android 防二次打包第一道锁:签名校验与完整性校验
android·kotlin
mmsx14 小时前
Android 混淆不等于安全:二次打包链路完整走一遍
android·kotlin