preface

As we all know, Activity methods such as onCreate(), onStart(), and onResume() all return a value of 0 with the view.getwidth () or view.getMeasureWidth() method. This is because, at the time these callback methods are called, the UI has not yet been drawn, which takes place after the onResume() method by executing the following method

ViewRootImpl. DoTraversal - > ViewRootImpl. PerformTraversals - > └ ViewRootImpl. RelayoutWindow └ ViewRootImpl. PerformMeasure └ ViewRootImpl. PerformLayout └ ViewRootImpl. PerformDraw ViewRootImpl. ReportDrawFinishedCopy the code

The usual solution is to use the view.post() method to send a task, which is executed after the UI is drawn. View.post() does exactly what it does

Now that we know when the UI will be drawn, but that’s not enough, we need to figure out how to accurately quantify how long the page will be drawn, looking for two points in time when the UI will be drawn and when it will be drawn. We can optimize startup and layout based on this time.

The starting point can be drawn from the onResume() method. The key is the selection of the end point. Article DoKit support Activity start time statistics scheme provides three ideas, we can analyze in detail.

Methods a

    Activity.java
    
    @Override
    protected void onResume(a) {
        super.onResume();
        final long start = System.currentTimeMillis();
        Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
            @Override
            public boolean queueIdle(a) {
                Log.d(TAG, "onRender cost:" + (System.currentTimeMillis() - start));
                return false; }}); }Copy the code

This method is relatively simple to implement, by adding idleHandler, send a task, the task will only be called when the thread is idle state

Method 2

    
    @Override
    protected void onResume(a) {
        super.onResume();
        final long start = System.currentTimeMillis();
        getWindow().getDecorView().post(new Runnable() {
            @Override
            public void run(a) {
                new Hanlder().post(new Runnable() {
                    @Override
                    public void run(a) {
                        Log.d(TAG, "onPause cost:"+ (System.currentTimeMillis() - start)); }}); }}); }Copy the code

This method first creates a task with view.post(), which will be executed after the UI is drawn, so why not just fetch the completion time in this task instead of sending a new task with a separate Handler? If we log each task and look at the execution time, we can see that they differ by tens to tens of milliseconds, and it is not accurate to get the drawing end time directly from the view.post() task. Let’s explore why.

Take a look at the source code for the view.post() method

	public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if(attachInfo ! =null) {
            return attachInfo.mHandler.post(action);
        }

        // Postpone the runnable until we know on which thread it needs to run.
        // Assume that the runnable will be successfully placed after attach.
        getRunQueue().post(action);
        return true;
    }
Copy the code

This first checks whether attachInfo is empty, otherwise handler.post() is called directly. That is, if the attachInfo object is not empty, view.post() and new Handler().post() have the same effect.

Otherwise, if attachInfo is empty, the POST () method of the mRunQueue object is called

	public void postDelayed(Runnable action, long delayMillis) {
        final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

        synchronized (this) {
            if (mActions == null) {
                mActions = new HandlerAction[4]; } mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction); mCount++; }}Copy the code

If you look at the source code for this method, you’ll see that instead of sending the task directly, it creates an array of HandlerActions to save. That is, if the attachInfo object is empty, the task is temporarily stored in the array and sent at a later point.

    ViewRootImpl.java
    
    private void performTraversals(a) {.../ / host DecorView namely
        host.dispatchAttachedToWindow(mAttachInfo, 0); . performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); . performLayout(lp, mWidth, mHeight); . performDraw(); . } View.java
    
    void dispatchAttachedToWindow(AttachInfo info, int visibility) { mAttachInfo = info; .// Transfer all pending runnables.
        if(mRunQueue ! =null) {
            mRunQueue.executeActions(info.mHandler);
            mRunQueue = null; }... }Copy the code

As you can see, within the dispatchAttachedToWindow method, all the previously saved tasks are sent by executing executeActions.

The dispatchAttachedToWindow method is sent before the performMeasure or other drawing operation, i.e. the task in view.post() is sent before the drawing operation. Why does it get the view’s true width and height?

This involves the Android messaging mechanism, the entire Android system is driven by messages, we only deal with the main thread here, so we send tasks through view.post(), new Handler().post(), etc. Both are added to the main thread to the message queue for execution, and the performTraversals() method is executed in a different task:

ViewRootImpl.java
	final class TraversalRunnable implements Runnable {
        @Override
        public void run(a) { doTraversal(); }}final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
    
	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

So tasks sent by the executeActions() method are simply added to the main thread to the task queue, and only when the performTraversals() task is finished will the other tasks in the queue be executed.

To return to the original problem, when we execute the view.post() method in onResume(), attachInfo is empty. So tasks in view.post() are temporarily stored in an array, sent to the main thread’s message queue before drawing, and executed after drawing. However, the cached task must not be the only one we added, there are other system tasks, so we need to add a new task at the end of the message queue of the main thread with new Handler().post() to mark the end of the drawing, which is relatively accurate.

Refer to the article

DoKit supports an Activity start time statistics scheme

Two problems with obtaining the width and height of a View via view.post ()

Handler synchronization barrier

View.post() does what it does