What happens between a UI control content change and being redrawn to the screen? Also, how many UI redraws do two settextViews in a row trigger? Why Android apps have a maximum frame rate of 60FPS is the subject of this article.

In movies, for example, animation needs to be at least 24FPS in order to keep the picture smooth, below which the naked eye will feel stuck. On mobile, this value is adjusted to 60FPS to increase the silkiness, which is why there is a (1000/60) 16ms indicator. Generally speaking, the maximum FPS on Android is 60, which is achieved through a VSYNC to ensure that at most one frame is drawn every 16ms. In short: the UI must wait at least 16ms before drawing the next frame, so two settextviews in a row only trigger a redraw. Let’s look at the UI redraw process in detail.

UI refresh process

Taking Textview as an example, when we change the contents of the Textview by setText, the UI interface will not change immediately. The APP side will first request the VSYNC service. After the next VSYNC signal is triggered, the UI of the APP side really starts to refresh

From our code side, it looks like this: SetText eventually calls invalidate to request a redraw, which is then recurred via ViewParent to the Invalidate of ViewRootImpl to request VSYNC. When requesting VSYNC, a synchronization barrier is added to prevent the UI thread from executing a synchronization message. In order to speed up the response of VSYNC, if not set, the UI update Task will be delayed when VSYNC arrives while a synchronization message is being executed. This is decided by Android Looper and MessageQueue.

The APP triggers redrawing, and the VSYNC application process is illustrated

When VSYNC arrives, the synchronization fence is removed and the current frame is processed first, using the following logic

VSYNC indicates the flow

DoFrame performs the UI drawing schematic

UI refresh source tracking

Just like a TextView, when the View changes, it usually calls invalidate to trigger a View redraw, so what happens? View will recursively call the parent’s invalidateChild, backtracking step by step to the ViewRootImpl invalidate as follows:

View.java

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) { // Propagate the damage rectangle to the parent view. 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); }Copy the code

ViewRootImpl.java

void invalidate() {
    mDirty.set(0, 0, mWidth, mHeight);
    if (!mWillDrawSoon) {
        scheduleTraversals();
    }
}
Copy the code

The ViewRootImpl will call scheduleTraversals in preparation for redrawing, but redrawing is usually not performed immediately, Instead, add a mTraversalRunnable to Choreographer’s Choreographer.CALLBACK_TRAVERSAL queue while applying VSYNC, This mTraversalRunnable will not be executed until the requested VSYNC is enabled as follows:

ViewRootImpl.java

// mTraversalScheduled is used to ensure that no Traversals are required before the current Traversals are executed, which takes 16ms. Void scheduleTraversals() {if (! mTraversalScheduled) { mTraversalScheduled = true; MTraversalBarrier = mhandler.getlooper ().getQueue().postSyncBarrier(); // postCallback, By request VNSC vertical synchronous signal scheduleVsyncLocked mChoreographer. PostCallback (Choreographer. CALLBACK_TRAVERSAL mTraversalRunnable, null); <! Add a callback to handle Touch events to prevent Touch events from coming --> if (! mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); }}Copy the code

Choreographer.java

private void postCallbackDelayedInternal(int callbackType, Object action, Object token, long delayMillis) { synchronized (mLock) { final long now = SystemClock.uptimeMillis(); final long dueTime = now + delayMillis; mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token); if (dueTime <= now) { <! ScheduleFrameLocked (now); }}}Copy the code

ScheduleTraversals Use mTraversalScheduled to ensure that scheduleTraversals will not be invoked again until the current mTraversalRunnable has been executed. Choreographer.CALLBACK_TRAVERSAL Theoretically should have only one mTraversalRunnable Task. MChoreographer. PostCallback insert mTraversalRunnable until after the CallBack, will then call scheduleFrameLocked request Vsync synchronization signal

ScheduleFrameLocked can be called several times, but mFrameScheduled uled guarantees that until the next vsync is scheduled. Private void scheduleFrameLocked(long now) {if (! mFrameScheduled) { mFrameScheduled = true; if (USE_VSYNC) { if (isRunningOnLooperThreadLocked()) { scheduleVsyncLocked(); } else {// Invalid already has synchronization fencing, mFrameScheduled, Message MSG = mhandler. obtainMessage(MSG_DO_SCHEDULE_VSYNC); msg.setAsynchronous(true); mHandler.sendMessageAtFrontOfQueue(msg); }}}}Copy the code

ScheduleFrameLocked, similar to the previous scheduleFrameLocked, uses mFrameScheduled uled to ensure that no new VSYNC is requested until the current one arrives, as it would be useless to request two vsyncs within 16ms. After arrival VSYNC, Choreographer use Handler will FrameDisplayEventReceiver encapsulated into an asynchronous Message, sent to the UI thread MessageQueue,

private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable { private boolean mHavePendingVsync; private long mTimestampNanos; private int mFrame; public FrameDisplayEventReceiver(Looper looper) { super(looper); } @Override public void onVsync(long timestampNanos, int builtInDisplayId, int frame) { long now = System.nanoTime(); if (timestampNanos > now) { <! TimestampNanos = now; timestampNanos = now; } <! -- If the previous vsync signal was not executed, If (mHavePendingVsync) {log.w (TAG, "Already have a pending vsync event. There should only be " + "one at a time."); } else { mHavePendingVsync = true; } <! TimestampNanos = timestampNanos; timestampNanos = timestampNanos; mFrame = frame; Message msg = Message.obtain(mHandler, this); <! --> msg.setasynchronous (true); mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS); } @Override public void run() { mHavePendingVsync = false; <! DoFrame (mFrame) is one of the most hypopanolic characters in the world. }}Copy the code

It is encapsulated as an asynchronous Message because a synchronization fence has been added so that synchronous messages are not executed. The UI thread is aroused, fetches the message, and eventually calls doFrame for a UI refresh redraw

void doFrame(long frameTimeNanos, int frame) { final long startNanos; synchronized (mLock) { <! -- a lot of things are done to ensure that there is one vSYNC signal, one input, one refresh, and one redraw at a time --> if (! mFrameScheduled) { return; // no work to do } long intendedFrameTimeNanos = frameTimeNanos; startNanos = System.nanoTime(); final long jitterNanos = startNanos - frameTimeNanos; <! -- Check if frame drop is due to delay. If (jitterNanos >= mFrameIntervalNanos) {final Long skippedFrames = jitterNanos/mFrameIntervalNanos; <! If (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {<! 2. Skipped over. I (TAG, "Skipped "+ skippedFrames +" frames! " + "The application may be doing too much work on its main thread."); } final long lastFrameOffset = jitterNanos % mFrameIntervalNanos; <! FrameTimeNanos = startNanos-lastFrameOffset; } if (frameTimeNanos < mLastFrameTimeNanos) { <! ScheduleVsyncLocked (); scheduleVsyncLocked(); scheduleVsyncLocked(); return; } <! IntendedFrameTimeNanos is the time stamp that was supposed to be drawn, frameTimeNanos is real, --> MFrameInfo. setVsync(intendedFrameTimeNanos, frameTimeNanos); <! --> mFrameScheduled = false; <! MLastFrameTimeNanos --> mLastFrameTimeNanos = frameTimeNanos; } try { <! --> trace.tracebegin (trace.trace_tag_view, "Choreographer#doFrame"); <! - processing packaging move events - > mFrameInfo. MarkInputHandlingStart (); doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); <! Animation - processing - > mFrameInfo. MarkAnimationsStart (); doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos); <! - handle redraw - > mFrameInfo. MarkPerformTraversalsStart (); doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); <! Choreographer.CALLBACK_COMMIT, frameTimeNanos); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); }}Copy the code

DoFrame also implemented a Boolean loop called mFrameScheduled uled to ensure only one redraw at a time in VSYNC. As you can see, several layers have been added to ensure only one redraw at 16ms. DoFrame handles a lot of other things besides UI redrawing, such as detecting how long VSYNC is delayed, how many frames are dropped, handling Touch events (usually moves), handling animation, and UI. It is when doFrame handles the mTraversalRunnable callback of Choreographer.CALLBACK_TRAVERSAL that the View redraw really begins:

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

Go back to ViewRootImpl and call doTraversal for View tree traversal,

Void doTraversal() {if (mTraversalScheduled) {mTraversalScheduled = false; <! Mhandler.getlooper ().getQueue().removesyncBarrier (mTraversalBarrier); performTraversals(); }}Copy the code

DoTraversal removes the fence and then handles performTraversals for measurement, layout, and rendering, and submits the current frame to SurfaceFlinger for display. The above Boolean variables guarantee a maximum of one UI redraw every 16ms, which is why Android currently has a limit of 60FPS.

Note: VSYNC synchronization signal will be received only after the user actively requests it, and it is valid once.

The UI is partially redrawn

The redrawing and refreshing of a certain View does not cause all views to be measure, layout and draw, but the link to be refreshed needs to be adjusted, and the rest of the View may not need to waste energy to do it again. The reaction on the APP side is: Do not need to call all ViewupdateDisplayListIfDirty build RenderNode rendering Op tree, as follows

View.java

public RenderNode updateDisplayListIfDirty() { final RenderNode renderNode = mRenderNode; . if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 || ! renderNode.isValid() || (mRecreateDisplayList)) { <! } else {<! - is still effective, without having to redraw - > mPrivateFlags | = PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID; mPrivateFlags &= ~PFLAG_DIRTY_MASK; } return renderNode; }Copy the code

conclusion

  • Android Max 60FPS is determined by VSYNC and maximum one frame per 16ms
  • VSYNC is available only if the client requests it
  • It will not refresh until VSYNC arrives
  • The UI does not change, VSYNC is not requested and therefore not refreshed
  • UI local redrawing is really just a way of eliminating the need to re-build the DrawOp tree for hardware acceleration.

Author: reading the little snail

Android VSYNC (Choreographer) and UI Refresh Principle Analysis. Md

For reference only, welcome correction