preface

Android will refresh the screen once every 16.6ms, which is often called 60fpx. So this is a problem:

16.6ms Refresh once is what once, is this fixed frequency to redraw? However, the timing of the code to draw is different. If the operation is to draw at the end of 16.6ms, isn’t the time less than 16.6ms, and there will also be the problem of frame loss? In addition, those familiar with drawing know that the drawing request is a Message object, so will this Message be put into the queue of main thread Looper? How can we guarantee that this Message will be executed within 16.6ms?

The article is long, please watch patiently, level is insufficient, if wrong, also hope to point out

View ## invalidate()

Since it’s drawing, let’s start with this method

public void invalidate(a) {
        invalidate(true);
    }
    public void invalidate(boolean invalidateCache) {
        invalidateInternal(0.0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }
    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {...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

So what is this p? ViewParent is an interface, so obviously p is an implementation class. The answer is ViewRootImpl. We know that the root of the View tree is a DecorView, The Parent of the DecorView is ViewRootImpl

If you’re familiar with the Activity launch process, you know that an Activity starts in an ActivityThread, The handleLaunchActivity() is executed indirectly to the Activity’s onCreate(), onStart(), and onResume() in turn. After doing this, ActivityThread calls WindowManager#addView(), and this addView() ends up calling the addView() method of WindowManagerGlobal, so let’s start here. Because it is a hidden class, here you look at Windows ManagerGlobal using Source Insight

public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
        synchronized (mLock) {
            .....
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true); }}throwe; }}public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {... view.assignParent(this); . }}void assignParent(ViewParent parent) {
        if (mParent == null) {
            mParent = parent;
        } else if (parent == null) {
            mParent = null; }}Copy the code

The argument is ViewParent, so it binds the DecorView directly to the ViewRootImpl, so it validates the conclusion that invalidate() in a child View will end up in the ViewRootImpl

ViewRootImpl##scheduleTraversals

ScheduleTraversals is executed based on the link above

void scheduleTraversals(a) {
        if(! mTraversalScheduled) { mTraversalScheduled =true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
Copy the code

It’s not long. First of all, if mTraversalScheduled is false, enter the judgment, and the position of this flag is true, I’ll leave the second part alone, we’ll talk about it later, but the postCallback method, it passes a mTraversalRunnable object, You can see here is a Runnable object that is being drawn

final class TraversalRunnable implements Runnable {
        @Override
        public void run(a) { doTraversal(); }}void doTraversal(a) {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
    mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }
            performTraversals();
            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false; }}}Copy the code

In doduletraversals, mTraversalScheduled has been set to false, and then there’s a postSyncBarrier(), And here again, removeSyncBarrier(), there’s actually a very interesting thing involved here, it’s called a synchronization barrier, which I’ll pull up in a minute, and then performTraversals(), which you should already know, The measurement, layout, and drawing of the View are initiated in this method. The code logic is too much, so I won’t post it. For the time being, just know that this method is the beginning of the measurement initiation.

So just to summarize for the moment, when a subview calls invalidate, it ends up calling performTraversals() of the ViewRootImpl, and performTraversals() is called inside doTraversal, DoTraversal is encapsulated in mTraversalRunnable, so what’s the execution time of this Runnable

Choreographer##postCallback

Returning to scheduleTraversals above, mTraversalRunnable is passed into Choreographer’s postCallback method

private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        if (DEBUG_FRAMES) {
        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); }}}Copy the code

You can see that there is something like a MessageQueue inside that stores the Runnable by delay time. Because the delay we passed in here is 0, we execute the scheduleFrameLocked(now) method

private void scheduleFrameLocked(long now) {
        if(! mFrameScheduled) { mFrameScheduled =true;
                if (isRunningOnLooperThreadLocked()) {
                    scheduleVsyncLocked();
                } else {
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                    msg.setAsynchronous(true); mHandler.sendMessageAtFrontOfQueue(msg); }}}private boolean isRunningOnLooperThreadLocked(a) {
        return Looper.myLooper() == mLooper;
    }
Copy the code

There is a judgment isRunningOnLooperThreadLocked, looked like a judge whether the current thread is the main thread, and if so, call scheduleVsyncLocked () method, if not will send a MSG_DO_SCHEDULE_VSYNC message, But eventually this method will be called

public void scheduleVsync(a) {
        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

If mReceiverPtr is not equal to 0, it will call nativeScheduleVsync(mReceiverPtr). This is a native method and will not trace to C++ for now

Previously, the CallBack was stored in a Queue, so there must be a method to execute it

void doCallbacks(int callbackType, long frameTimeNanos) {
        CallbackRecord callbacks;
        synchronized (mLock) {
        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

Let’s see where this method was called, inside the doFrame method, okay

void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
        try{... mFrameInfo.markInputHandlingStart(); doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); mFrameInfo.markAnimationsStart(); doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos); mFrameInfo.markPerformTraversalsStart(); doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos); }finally{ AnimationUtils.unlockAnimationClock(); Trace.traceEnd(Trace.TRACE_TAG_VIEW); }... }Copy the code

So here’s the key: DoFrame (), which is a method that executes a task based on a timestamp, and that task is the doTraversal() operation wrapped in the ViewRootImpl, DoTraversal () calls performTraversals() to start measuring, arranging, and drawing the entire View tree as needed. So the only question left is where the doFrame() method is called.

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) {     
                scheduleVsync();
                return;
            }
            mTimestampNanos = timestampNanos;
            mFrame = 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; doFrame(mTimestampNanos, mFrame); }}Copy the code

You can see that in the onVsync callback, Message MSG = message.obtain (mHandler, this) is passed to this, which then executes into the run method, which in turn executes the doFrame method, so eventually the problem occurs. This onVsync(), when did this callback come back

FrameDisplayEventReceiver inherited from DisplayEventReceiver VSync signal began to receive the bottom handle UI process. The VSync signal is implemented by SurfaceFlinger and sent periodically. FrameDisplayEventReceiver after receiving the signal, call onVsync method group message is sent to the main thread processing. The main content of this message is the doFrame in the run method, where the mTimestampNanos is the time parameter of the signal arrival.

That is, every 16.6ms, the bottom layer sends out a screen refresh signal, and then calls back to the onVsync method. However, there is a strange thing. How does the bottom layer know which app needs this signal to refresh? To implement this normal observer mode, register the app itself, but I don’t seem to see where there is a way to register the app at the bottom. Again, back to the native method above, nativeScheduleVsync(mReceiverPtr), So basically what this method does is register listeners,

Synchronization barrier

To summarize, when we call invalidate(), requestLayout(), etc to refresh the interface, we don’t immediately refresh the interface. Instead, scheduleTraversals() of the ViewRootImpl registers with the underlying layer to listen for the next screen refresh event, and then when the next screen refresh event arrives, This is performed by performTraversals() traversals through the drawn View tree.

Looper will retrieve a Message object from the MessageQueue. After processing a Message, Looper will retrieve a Message object from the MessageQueue. So how can the drawn Message be executed within 16.6ms as much as possible?

So here we have a synchronization barrier thing, so let’s go back to the scheduleTraversals code

void scheduleTraversals(a) {
        if(! mTraversalScheduled) { mTraversalScheduled =true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
Copy the code

Mhandler.getlooper ().getQueue().postsyncbarrier (), which is not analyzed above, goes into the method

private int postSyncBarrier(long when) {
        synchronized (this) {
            final int token = mNextBarrierToken++;
            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            msg.arg1 = token;
            Message prev = null;
            Message p = mMessages;
            if(when ! =0) {
                while(p ! =null&& p.when <= when) { prev = p; p = p.next; }}if(prev ! =null) { // invariant: p == prev.next
                msg.next = p;
                prev.next = msg;
            } else {
                msg.next = p;
                mMessages = msg;
            }
            returntoken; }}Copy the code

If you are familiar with the Handler process, you should know that the final Message is sent through MSG. Target

Let’s go back to MessageQueue’s next method

Message next(a) {
        for(;;) {...synchronized (this) {...// Target ==null
                if(msg ! =null && msg.target == null) {                 
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while(msg ! =null && !msg.isAsynchronous());
                }
                if(msg ! =null) {
                    if (now < msg.when) {                     
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if(prevMsg ! =null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        returnmsg; }}else {                
                    nextPollTimeoutMillis = -1; }}}Copy the code

Target ==null. The do while loop iterates through the list of messages. When the loop breaks, MSG points to the Message closest to the header

As you can see, when the synchronization barrier is set, the next function will ignore all synchronous messages and return asynchronous messages. In other words, once the synchronization barrier is in place, the Handler will only handle asynchronous messages. In other words, the synchronization barrier adds a simple priority mechanism to the Handler message mechanism, with asynchronous messages taking precedence over synchronous messages

In this case, the drawn Message can be fetched and executed as quickly as possible, because scheduleTraversals() is only sent to the Message queue when it is called, so only when a View requests a refresh. Only after this point will synchronous messages be intercepted. If the work was sent to the message queue before scheduleTraversals(), it will still be carried out in sequence

conclusion

  • View refresh requests are sent to scheduleTraversals() of the ViewRootImpl, which forms a Message through a Runnable, and then sends a synchronization barrier that intercepts all synchronized and asynchronous messages, so that the refresh task can be executed as quickly as possible
  • It is often said that the screen is refreshed every 16.6ms. In fact, the bottom layer will switch each frame at this frequency. Only when the View initiates the refresh request, the App will register with the bottom layer to listen for the next screen refresh signal, and can receive the notification of the next signal to call back onVsync
  • App is responsible for the calculation of the screen refresh data, but not immediately after the completion of the refresh data, more depends on whether or not the next bottom to refresh the screen command callback time, so will answer the above questions, each time order arrived to refresh the data, as far as possible the tasks that guarantee the refresh data have enough time of 16.6 ms
  • 1. The task of drawing the View tree is longer than 16.6ms. At this time, the next signal comes, resulting in the loss of frame 2. Although the method of synchronization barrier is adopted to ensure enough time for View drawing, if the Message between synchronization barriers takes too long, the work of traversing View tree can not start, so that the time of switching the next frame in the bottom layer of 16.6ms is exceeded. This is why you don’t want to do time-consuming operations on the main thread

The resources

Decipher the 16ms problem in Android performance optimization

Handler synchronization barrier mechanism