“This is the second day of my participation in the November Gwen Challenge. See details of the event: The last Gwen Challenge 2021”.

Choreographer work process

Google introduced Choreographer after 4.1. The Chinese translation for Choreographer is Choreographer, Choreographer. Choreographer’s job for screen refreshes is to initiate and receive the underlying VSync signals. Workflow diagram

ViewRootImpl#requestLayout

The view requestLayout will eventually call ViewRootImpl#requestLayout, and the parent of the DecorView is ViewRootImpl

@Override public void requestLayout() { if (! MHandlingLayoutInLayoutRequest) {/ / check whether in the UI update request the main thread by checkThread (); mLayoutRequested = true; scheduleTraversals(); } } void scheduleTraversals() { if (! MTraversalScheduled) {// This flag prevents multiple calls to the method, which will be reset when VSync is received and processed or when the UI update request is cancelled; // Initiate a synchronization barrier message mTraversalBarrier = mhandler.getlooper ().getQueue().postsyncBarrier (); / / send a message to monitor vertical synchronization information mChoreographer postCallback (Choreographer. CALLBACK_TRAVERSAL mTraversalRunnable, null); if (! mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } / / send a message to inform notifyRendererOfFramePending (); pokeDrawLockIfNeeded(); }}Copy the code

Summary: ViewRootImpl’s main job when asking Choreographer for an update to the UI is

  1. Detects the current thread initiating the UI update.
  2. Send a synchronization barrier to the message queue. When there is a synchronization barrier in the message queue, asynchronous messages are processed first, and UI update messages are asynchronous messages.

Choreographer

Choreographer#postCallback

public void postCallback(int callbackType, Runnable action, Object token) { postCallbackDelayed(callbackType, action, token, 0); } public void postCallbackDelayed(int callbackType, Runnable action, Object token, long delayMillis) { if (action == null) { throw new IllegalArgumentException("action must not be null"); } if (callbackType < 0 || callbackType > CALLBACK_LAST) { throw new IllegalArgumentException("callbackType is invalid");  } postCallbackDelayedInternal(callbackType, action, token, delayMillis); } 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); // Generally this is the position to go, but in the TextView a delayed message is sent. 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); }}}Copy the code

Choreographer#scheduleFrameLocked

private void scheduleFrameLocked(long now) { if (! mFrameScheduled) { mFrameScheduled = true; If (USE_VSYNC) {if (DEBUG_FRAMES) {log. d(TAG, "Scheduling next frame on vsync."); } // If running on the Looper thread, then schedule the vsync immediately, // otherwise post a message to schedule the vsync from the UI thread // as soon as possible. if (isRunningOnLooperThreadLocked ()) {/ / if it is in the main thread, direct call is not in the main thread if the call handler messages sent scheduleVsyncLocked scheduleVsyncLocked (); } else { Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC); msg.setAsynchronous(true); mHandler.sendMessageAtFrontOfQueue(msg); } } else { final long nextFrameTime = Math.max( mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now); if (DEBUG_FRAMES) { Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms."); } Message msg = mHandler.obtainMessage(MSG_DO_FRAME); msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, nextFrameTime); }}}Copy the code

Choreographer#scheduleVsyncLocked

private void scheduleVsyncLocked() {
        mDisplayEventReceiver.scheduleVsync();
    }
Copy the code

FrameDisplayEventReceiver

MDisplayEventReceiver FrameDisplayEventReceiver is an instance of the object, its scheduleVsync method in the superclass DisplayEventReceiver FrameDisplayEventReceiver

DisplayEventReceiver#scheduleVsync

public void scheduleVsync() { if (mReceiverPtr == 0) { Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event " + "receiver has already been disposed."); } else { nativeScheduleVsync(mReceiverPtr); }}Copy the code

The underlying implementation of nativeScheduleVsync is to reset the flag of whether to distribute VSync signals and wait for the next VSync message to be received. After the VSync message is distributed, the flag will be reset. Therefore, the VSync message will not be received every time.

DisplayEventReceiver#dispatchVsync

// Called from native code.
    @SuppressWarnings("unused")
    private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {
        onVsync(timestampNanos, builtInDisplayId, frame);
    }
​
​
Copy the code

DisplayEventReceiver#dispatchVsync is responsible for receiving vSYNC information from the underlying call and calling onVsync.

FrameDisplayEventReceiver

In DisplayEventReceiver onVsync is empty, in its subclasses FrameDisplayEventReceiver specific logic

private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable { private boolean mHavePendingVsync; private long mTimestampNanos; private int mFrame; public FrameDisplayEventReceiver(Looper looper, int vsyncSource) { super(looper, vsyncSource); } @Override public void onVsync(long timestampNanos, int builtInDisplayId, int frame) { // Ignore vsync from secondary display. // This can be problematic because the call to scheduleVsync() is a  one-shot. // We need to ensure that we will still receive the vsync from the primary // display which is the one we really care about. Ideally we should schedule // vsync for a particular display. // At this time Surface Flinger won't send us vsyncs for secondary displays // but that could change in the future so let's log a message to help us remember // that we need to fix this. if (builtInDisplayId ! = SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) { Log.d(TAG, "Received vsync from secondary display, but we don't support " + "this case yet. Choreographer needs a way to explicitly request " + "vsync for a specific display to ensure it doesn't lose track " + "of its scheduled vsync."); scheduleVsync(); return; } // 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) {// Correct the timestamp, 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; // The second argument is that this eventually executes its own run Message MSG = message.obtain (mHandler, this); msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS); } @Override public void run() { mHavePendingVsync = false; doFrame(mTimestampNanos, mFrame); }}Copy the code

It can be seen that the underlying vSYNC signal and doFrame will pass through the handler to switch the processing of the message to the main thread. If the main thread is performing a time-consuming operation at this time, the synchronization barrier is guaranteed, but the frame may be lost.

Choreographer

DoFrame is an internal method of Choreographer

Choreographer#doFrame

void doFrame(long frameTimeNanos, int frame) { final long startNanos; synchronized (mLock) { if (! mFrameScheduled) { return; // no work to do } if (DEBUG_JANK && mDebugPrintNextFrameTimeDelta) { mDebugPrintNextFrameTimeDelta = false; Log.d(TAG, "Frame time delta: "+ ((frameTimeNanos -mlastFrametimenanos) * 0.00000f) +" ms"); } long intendedFrameTimeNanos = frameTimeNanos; startNanos = System.nanoTime(); // DoFrame calls are too slow. Final Long jitterNanos = startNanos-frametimenanos; //mFrameIntervalNanos is calculated based on the screen refresh frequency, If (jitterNanos >= mFrameIntervalNanos) {final Long skippedFrames = jitterNanos / mFrameIntervalNanos; // This can detect the time-consuming operation after initiating the UI update, 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 % mFrameIntervalNanos; if (DEBUG_JANK) { Log.d(TAG, "Missed vsync by "+ (jitterNanos * 0.00000f) +" ms "+ "which is more than the frame interval of" + (mFrameIntervalNanos * 0.00000f); "+ "Skipping" + "skippedFrames" + "frame and setting frame" + "time to "+ (lastFrameOffset * 0.000001f) +" ms in the past."); } frameTimeNanos = startNanos - lastFrameOffset; } 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."); } scheduleVsyncLocked(); return; } if (mFPSDivisor > 1) { long timeSinceVsync = frameTimeNanos - mLastFrameTimeNanos; if (timeSinceVsync < (mFrameIntervalNanos * mFPSDivisor) && timeSinceVsync > 0) { scheduleVsyncLocked(); return; } } mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos); mFrameScheduled = false; mLastFrameTimeNanos = frameTimeNanos; } try { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame"); AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS); mFrameInfo.markInputHandlingStart(); doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); mFrameInfo.markAnimationsStart(); doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos); mFrameInfo.markPerformTraversalsStart(); // Type doCallbacks passed by requestLayout (Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos); } finally { AnimationUtils.unlockAnimationClock(); Trace.traceEnd(Trace.TRACE_TAG_VIEW); } if (DEBUG_FRAMES) { final long endNanos = System.nanoTime(); Log.d(TAG, "Frame " + frame + ": Finished, took "+ (Endnanos-startnanos) * 0.00000f +" ms, Latency "+ (startNanos-frameTimenanos) * 0.00000f +" Ms. "); }}Copy the code

The algorithm for calculating how many frames are lost is: Offset time/time per frame The time per frame is calculated based on the screen refresh rate. For Android devices, the time per frame is mostly 60 but there are others

Choreographer#doCallbacks

void doCallbacks(int callbackType, long frameTimeNanos) { 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; if (callbackType == Choreographer.CALLBACK_COMMIT) { final long jitterNanos = now - frameTimeNanos; Trace.traceCounter(Trace.TRACE_TAG_VIEW, "jitterNanos", (int) jitterNanos); if (jitterNanos >= 2 * mFrameIntervalNanos) { final long lastFrameOffset = jitterNanos % mFrameIntervalNanos + mFrameIntervalNanos; if (DEBUG_JANK) { Log.d(TAG, "Commit callback delayed by "+ (jitterNanos * 0.00000f) +" ms which is more than twice the frame interval of "+ (mFrameIntervalNanos * 0.00000f); "+ "Setting frame time to" + (lastFrameOffset * 0.00000f) + "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); }}Copy the code

ViewRootImpl$TraversalRunnable

The CallbackRecord assembled during requestLayout will eventually be called to ViewRootImpl$TraversalRunnable

final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); }}Copy the code

ViewRootImpl#doTraversal

void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; // Remove the synchronization barrier mhandler.getLooper ().getQueue().removesyncBarrier (mTraversalBarrier); if (mProfile) { Debug.startMethodTracing("ViewAncestor"); } // Start View traversals (); if (mProfile) { Debug.stopMethodTracing(); mProfile = false; }}}Copy the code

Choreographer accepts screen refresh requests initiated by ViewRootImpl on top and calls ViewRootImpl on bottom for UI updates on the basis of listening for vSYNC signals.

Summary: Reasons for app lag

External:

Memory is tight and frequent GC causes UI threads to be suspended

Other threads/processes frequently preempted the CPU, causing the app to stall because it could not get the CPU

Internal:

A time-consuming operation exists in the message queue, causing doFrame to fail to execute the delay in time

The VIEW layout process takes time. As a result, the CPU cannot prepare data required by the GPU in a timely manner

There are other asynchronous messages in the message queue, causing doFrame not to execute in time (we use asynchronous messages cautiously in app development).