The Android interface is stuck because frames drop, and frames drop because the speed of producing frames can’t keep up with the speed of consuming frames.

The consumption rate of frames is linked to the screen refresh rate. The screen is like a comic strip. If 60 frames are played in a second, the consumption rate of one frame is 1000/60 = 16.6ms, that is, the screen will remove the display content of the next frame every 16.6ms. It sounds like losing something that can’t be found, but it actually describes the behavior of screen hardware by “missing an opportunity to display content.”

Why does the screen not fetch the display content? You have to look at the software. With that in mind, read the Framework source code.

This is the fifth article in the long knowledge series of read source code.

  1. Read the source code long knowledge better RecyclerView | click listener

  2. Android custom controls | source there is treasure in the automatic line feed control

  3. Android custom controls | three implementation of little red dot (below)

  4. Reading knowledge source long | dynamic extension class and bind the new way of the life cycle

  5. Reading knowledge source long | Android caton true because “frame”?

Choreographer

The ViewRootImpl is the root View for all views in an Activity, which initiates the View tree traversal:

public final class ViewRootImpl implements ViewParent.View.AttachInfo.Callbacks.ThreadedRenderer.DrawCallbacks {
    / / choreographer
    Choreographer mChoreographer;
    // Trigger to traverse the View tree
    void scheduleTraversals(a) {
        if(! mTraversalScheduled) { mTraversalScheduled =true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // Throw View tree traversal tasks to Choreographer
            mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
    
    // Walk through the View tree task
    final class TraversalRunnable implements Runnable {
        @Override
        public void run(a) { doTraversal(); }}// Build the traversal View tree task instance
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
}
Copy the code

The ViewRootImpl triggers View tree traversal by throwing tasks to Choreographer.

Choreographer is a class under the Android. view package that literally means “Choreographer”, which implicitly means “need to synchronize the two”, Choreographer is about synchronizing movement with rhythm. Choreographer is about synchronizing “draw content” with “vSYNC signals”.

Storing the drawing task

public final class Choreographer {
    // Enter the task
    public static final int CALLBACK_INPUT = 0;
    // Animation task
    public static final int CALLBACK_ANIMATION = 1;
    // View tree traverses tasks
    public static final int CALLBACK_TRAVERSAL = 2;
    / / COMMIT task
    public static final int CALLBACK_COMMIT = 3;
    // Hold a chained array of tasks
    private final CallbackQueue[] mCallbackQueues;
    // Main thread message handler
    private final FrameHandler mHandler;
    
	// Discard the drawing task
	public void postCallback(int callbackType, Runnable action, Object token) {
        postCallbackDelayed(callbackType, action, token, 0);
    }

	// Delay throwing the drawing task
    public void postCallbackDelayed(int callbackType,
            Runnable action, Object token, long delayMillis) {... postCallbackDelayedInternal(callbackType, action, token, delayMillis); }// Throw the concrete implementation of the drawing task
    private void postCallbackDelayedInternal(int callbackType, Object action, Object token, long delayMillis) {
        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            // 1. Temporarily store the drawing task in the chain structure according to the type
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            // 2. Subscribe to the next vSYNC signal
            if (dueTime <= now) {
            	// Subscribe to the next vSYNC signal immediately
                scheduleFrameLocked(now);
            } else {
            	Subscribe to vSYNC signals at some point in the future
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, dueTime); }}}// Main thread message handler
    private final class FrameHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
				...
                case MSG_DO_SCHEDULE_CALLBACK:
                    Subscribe to vSYNC signal at future point in time
                    doScheduleCallback(msg.arg1);
                    break; }}}void doScheduleCallback(int callbackType) {
        synchronized (mLock) {
            if(! mFrameScheduled) {final long now = SystemClock.uptimeMillis();
                if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
                    // Subscribe to the next vSYNC signal
                    scheduleFrameLocked(now);
                }
            }
        }
    }
}
Copy the code

After Choreographer receives a new draw task, it performs two actions:

  1. Draw tasks into the chain:
public final class Choreographer {
    // Draw the task chain
    private final class CallbackQueue {
        // Task header
        private CallbackRecord mHead;
        
        // Draw tasks into the chain (in ascending order of time)
        public void addCallbackLocked(long dueTime, Object action, Object token) {
            CallbackRecord callback = obtainCallbackLocked(dueTime, action, token);
            CallbackRecord entry = mHead;
            if (entry == null) {
                mHead = callback;
                return;
            }
            / / insert
            if (dueTime < entry.dueTime) {
                callback.next = entry;
                mHead = callback;
                return;
            }
            
            // Middle insert or tail insert
            while(entry.next ! =null) {
                if (dueTime < entry.next.dueTime) {
                    callback.next = entry.next;
                    break; } entry = entry.next; } entry.next = callback; }}// Draw the task node
    private static final class CallbackRecord {
    	// Next drawing task
        public CallbackRecord next;
        // The drawing task should be executed at this point
        public long dueTime;
        // Describe the code snippet for the drawing task
        publicObject action; .// Perform the drawing task
        public void run(long frameTimeNanos) {
            if (token == FRAME_CALLBACK_TOKEN) {
                ((FrameCallback)action).doFrame(frameTimeNanos);
            } else{ ((Runnable)action).run(); }}}}Copy the code

Choreographer accepts four types of tasks, namely input, animation, View tree traversal, and COMMIT. Each task is abstracted into a CallbackRecord, and similar tasks form a task chain CallbackQueue in chronological order. Four task chains are stored in the mCallbackQueues[] array structure.

  1. Subscribe to the next vSYNC signal
public final class Choreographer {
    private void scheduleFrameLocked(long now) {
    	// Do nothing if you have subscribed to the next vSYNC signal
        if(! mFrameScheduled) {// When the next vSYNC signal arrives, the drawing task needs to be performed
            mFrameScheduled = true;
            if (USE_VSYNC) {
            	ScheduleVsyncLocked () is eventually called to register for receiving vSYNC signals regardless of which branch you go
                if (isRunningOnLooperThreadLocked()) {
                    scheduleVsyncLocked();
                } else {
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                    msg.setAsynchronous(true); mHandler.sendMessageAtFrontOfQueue(msg); }}else{... }}}// Delegate DisplayEventReceiver to register vSYNC signals
    private void scheduleVsyncLocked(a) {
        mDisplayEventReceiver.scheduleVsync();
    }
        
    // Draw a frame
    void doFrame(long frameTimeNanos, int frame) {
        synchronized (mLock) {
            // If you do not need to respond to the vSYNC signal, it returns directly, without drawing anything
            if(! mFrameScheduled) {return; }... }... }private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
	// VSYNC signal callback
        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {...// Throw a task to the main thread, draw a frame
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run(a) {
            mHavePendingVsync = false;
            // Draw current framedoFrame(mTimestampNanos, mFrame); }}}// VSYNC receiver
public abstract class DisplayEventReceiver {
    // Register to receive the next vSYNC signal
    public void scheduleVsync(a) {
        if (mReceiverPtr == 0) {
            Log.w(TAG, "...");
        } else {
        	Subscribe to SurfaceFlinger for the next vSYNC signalnativeScheduleVsync(mReceiverPtr); }}... }Copy the code

Not every VSYNC signal will be subscribed to and processed by the upper layer, and only when Choreographer subscribed to the next VSYNC signal will SurfaceFlinger call back to the upper layer via onVsync().

Before the first vertical signal in the figure arrives, postCallback() is called to throw a draw task to Choreographer, subscribs to the next signal and sets mFrameScheduled to true, indicating that the next frame needs to be drawn. When the first signal is generated, onVsync() will be called back, doFrame() will be thrown to the main thread, and mFrameScheduled uled to false. Since there are no further subscriptions, no onVsync() call-back will be received, and nothing new will be drawn.

Perform a drawing task

Once ViewRootImpl throws a draw task to Choreographer, the task does not immediately execute, but is temporarily stored in the draw task chain and registered to receive the next vSYNC signal. It will only be executed after the next signal passes the onVsync() callback:

public final class Choreographer {
    // VSYNC receiver
    private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
        private boolean mHavePendingVsync;
        private long mTimestampNanos;
        private int mFrame;

        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {...// Send an asynchronous message to the main thread and execute the current Runnable, i.e. DoFrame ()
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run(a) {
            mHavePendingVsync = false;
            // Draw a frame of contentdoFrame(mTimestampNanos, mFrame); }}}Copy the code

Every time the vSYNC signal is called back, doFrame() is pushed to the main thread.

public final class Choreographer {
    void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
            // If there is no vSYNC signal for this frame, exit without drawing
            if(! mFrameScheduled) {return; // no work to do}...try {
            // Handle the input event for this frame
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
            // Animate this frame
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
            // Handle the View tree traversal for this frame
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
	    // Execute the COMMIT task after all drawing tasks are complete
            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } finally{... }... }}Copy the code

As each frame is drawn, tasks are processed in the order of “Input event”, “animation”, “View tree traversal”, and “COMMIT”.

The handler doCallback() is defined as follows:

public final class Choreographer {
    // Hold a chained array of draw tasks
    private final CallbackQueue[] mCallbackQueues;
    
    void doCallbacks(int callbackType, long frameTimeNanos) {
        CallbackRecord callbacks;
        synchronized (mLock) {
            final long now = System.nanoTime();
            // Fetches tasks from the specified type of task chain based on the current point in time
            callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now / TimeUtils.NANOS_PER_MS);
            if (callbacks == null) {
                return; }... }try {
            // Execute all tasks removed from the task chain
            for(CallbackRecord c = callbacks; c ! =null; c = c.next) { c.run(frameTimeNanos); }}finally{... }}// Task entity class
    private static final class CallbackRecord {
        public CallbackRecord next;
        publicObject action; .public void run(long frameTimeNanos) {
            if (token == FRAME_CALLBACK_TOKEN) {
                ((FrameCallback)action).doFrame(frameTimeNanos);
            } else {
            	// Execute the task((Runnable)action).run(); }}}}Copy the code

At this point, the “View tree traversal “task pushed by ViewRootImpl was finally executed.

Which extractDueCallbacksLocked () is the method of task chain CallbackQueue, used to get to the current time point need to be performed all tasks:

private final class CallbackQueue {
    // Task header
    private CallbackRecord mHead;

    // Get all tasks prior to the current time node that need to be executed in the current frame
    public CallbackRecord extractDueCallbacksLocked(long now) {
        CallbackRecord callbacks = mHead;
        if (callbacks == null || callbacks.dueTime > now) {
            return null;
        }

        CallbackRecord last = callbacks;
        CallbackRecord next = last.next;
        // Walk through the task chain, with the present time as the dividing line, from the past task chain
        while(next ! =null) {
            if (next.dueTime > now) {
                last.next = null;
                break;
            }
            last = next;
            next = next.next;
        }
        mHead = next;
        // Return all past tasks as a chain
        returncallbacks; }}Copy the code

When the current frame is drawn, all previous tasks are taken from the task chain with the current moment as the dividing line and executed one by one in chronological order.

Run the above code on paper:The figure above shows that before the first vSYNC signal arrives, the upper layer has added three tasks to the task chain. The first two tasks are executed before the first signal arrives, and the third task is executed after the first signal.

The subscription to the first signal is completed when the first task enters the chain, and the third task is executed after the first signal, so its subscription behaviordoScheduleCallback()It is stored in the main thread message queue until the first signal arrives and then subscribes to the second signal.When the first vSYNC signal arrives,doFrame()Thrown to the main thread, it picks up tasks 1 and 2 from the task chain before the current time node and executes them. When the task is complete, the next message “subscribe to next vSYNC signal for Task 3” is fetched from the main thread message queue and executed. When all this is done, the main thread can only sit in a daze, waiting for the next vSYNC signal.

When the second vSYNC signal arrives, the remaining tasks 3 in the task chain are removed and thrown to the main thread for execution. The task chain is empty. When task 3 is finished, the main thread is completely idle and can only wait for the upper layerChoreographerThe task.

Is the lag due to frame drop?

delay

In both cases, the drawing task can be completed in one frame interval. What happens if the task is time-consuming and takes longer than the interval?

After the first vertical signal arrives, tasks 1 and 2 are thrown to the main thread for execution, this time taking slightly longer than one frame interval, causing the function to subscribe to the next signal to be delayed. For the top, miss the secondonVsync()Callback, which means that tasks 1 and 2 missed a display opportunity, and task 3 missed a render opportunity. As for the bottom layer, when the monitor sends a vSYNC signal, it fetches the display from the graphics buffer. This time it doesn’t get the display, so it continues to display the previous frame.The main thread does not “subscribe to the next signal” until tasks 1 and 2 are finished. When the third signal arrives, the display retrits the rendering results of tasks 1 and 2 from the graphics buffer, and task 3 is thrown to the main thread for execution.

In this case, all drawing tasks in the task chain are displayed deferred.

merge

What if there are other time-consuming tasks in the main thread besides drawing?

Assume that before the arrival of the first vSYNC signal, the upper layer has already thrown three drawing tasks into the task chain, and their execution time is respectively set between the first, second and three VSYNC signals.When the first vSYNC signal arrives,doFrame()Is thrown to the main thread message queue, but the main thread is occupied by an I/O taskdoFrame()Never got the chance to execute. And functions that subscribe to subsequent signals can’t be executed, so second and thirdonVsync()Will not be called until the I/O operation completes.

How will delayed doFrame() be different?

public final class Choreographer {
    private Choreographer(Looper looper, int vsyncSource) {...// Frame interval = 1 second/refresh rate
        mFrameIntervalNanos = (long) (1000000000 / getRefreshRate());
    }
    
    private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
        private long mTimestampNanos;

        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {...// Record the arrival time of the vertical signalmTimestampNanos = timestampNanos; . Message msg = Message.obtain(mHandler,this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run(a) {
            mHavePendingVsync = false;
            // The vertical signal is thrown to the main thread along with doFrame() when it arrivesdoFrame(mTimestampNanos, mFrame); }}void doFrame(long frameTimeNanos, int frame) {
        synchronized (mLock) {
            ...
            // The time when the current frame is actually drawn
            startNanos = System.nanoTime();
            // Calculate the current frame drawing delay = current moment - the moment when it should have been drawn
            final long jitterNanos = startNanos - frameTimeNanos;
            // If the delay is greater than the frame interval
            if (jitterNanos >= mFrameIntervalNanos) {
            	// Count the number of frames skipped and log warning if the number of frames exceeds the threshold
                final long skippedFrames = jitterNanos / mFrameIntervalNanos;
                if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
                    Log.i(TAG, "Skipped " + skippedFrames + " frames! "
                            + "The application may be doing too much work on its main thread.");
                }
                // Calculates the offset of the frame delay with respect to the frame interval
                final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
				// Correct the frame drawing time to align it with the latest arrival vSYNC signalframeTimeNanos = startNanos - lastFrameOffset; }...// Update the last drawing time to the latest vSYNC signal time
            mLastFrameTimeNanos = frameTimeNanos;
        }

        try {
        	// Render the current frame (the frameTimeNanos passed in is the latest vSYNC signal moment)
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } finally{... }... }}Copy the code

Before drawing a frame, doFrame() calculates the frame delay, which is the difference between the current moment and the moment the frame should have been drawn.

If the frame delay is greater than the frame interval, the draw time frameTimeNanos is corrected to align it to a vsync signal moment prior to the current time. That moment is then passed to doCallbacks() as the basis for picking tasks off the task chain, and all tasks prior to that point are picked off.

At this point, tasks 1, 2, and 3 will all be removed because they are past tasks relative to the subsequent frameTimeNanos, so this doFrame() execution will be performed at the same time as the previous three doFrame() drawing tasks. If the drawing task is executed fast enough, You have the opportunity to subscribe to the next vSYNC signal before it arrives, so that the contents drawn by tasks 1, 2, and 3 are displayed in the next frame, as shown below:

Frame drops occur when the main thread is occupied by time-consuming operations, meaning that frames that should have been rendered and displayed are missed. But for the upper level, nothing is lost, except that the drawing tasks that should have been performed at different frame intervals are combined and displayed together.

What happens if three drawing tasks are time-consuming?

At this point, certain conditions in doCallbacks() fire:

public final class Choreographer {
    void doFrame(long frameTimeNanos, int frame) {
        synchronized (mLock) {
            ...
            startNanos = System.nanoTime();
            final long jitterNanos = startNanos - frameTimeNanos;
            // If the delay is greater than the frame interval
            if (jitterNanos >= mFrameIntervalNanos) {
				...
                // Calculates the offset of the frame delay with respect to the frame interval
                final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
				// Correct the frame drawing time to align it with the latest vSYNC signalframeTimeNanos = startNanos - lastFrameOffset; }...// Update the last drawing time to the latest vSYNC signal time
            mLastFrameTimeNanos = frameTimeNanos;
            
            // If the frame is old relative to the previous frame, skip the current frame and subscribe to the next vSYNC signal
            if (frameTimeNanos < mLastFrameTimeNanos) {
                scheduleVsyncLocked();
                return; }}try {
        	// Render the current frame
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
            // After the input, animation, and View tree traversal tasks are completed, execute the COMMIT task
            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } finally{... }... }void doCallbacks(int callbackType, long frameTimeNanos) {
        CallbackRecord callbacks;
        synchronized (mLock) {
			...
            // If it is a COMMIT task
            if (callbackType == Choreographer.CALLBACK_COMMIT) {
            	// Computes the frame drawing delay
                final long jitterNanos = now - frameTimeNanos;
                // If the frame drawing delay is >= 2 times the frame interval
                if (jitterNanos >= 2 * mFrameIntervalNanos) {
                	// Calculates the frame offset and appends a frame interval
                    final long lastFrameOffset = jitterNanos % mFrameIntervalNanos + mFrameIntervalNanos;
                    // Set mLastFrameTimeNanos to the second vSYNC signal moment before the current momentframeTimeNanos = now - lastFrameOffset; mLastFrameTimeNanos = frameTimeNanos; }}}... }}Copy the code

Because the doFrame() execution took longer than 2 frames, when the input, animation, and View tree traversal tasks were completed and the COMMIT task was executed, the frame alignment was triggered and the mLastFrameTimeNanos was offset forward by some time. Align to the second vSYNC signal moment before the end of the drawing task. This is done to prevent more frames from being dropped due to this timeout drawing. Because doFrame() checks the drawing time of the current frame and the previous frame before drawing the current frame:

// If the frame is old relative to the previous frame, skip the current frame and subscribe to the next vSYNC signal
if (frameTimeNanos < mLastFrameTimeNanos) {
	scheduleVsyncLocked();
	return;
}
Copy the code

If Choreographer throws a number of draw tasks towards the end of the last doFrame(), the frameTimeNanos of these draw tasks must be smaller than the frameTimeNanos of the last frame. If mLastFrameTimeNanos is set forward without a COMMIT task, the new draw task will miss one execution.