Public number: byte array, hope to help you 馃ぃ馃ぃ

Post (Runnable) allows you to retrieve the width and height of a View from the onCreate method of your Activity.

Readers can try the following. Post (Runnable) is used to get the View’s true width and height

/ * * * the author: leavesC * time: 2020/03/14 description: his * * GitHub:https://github.com/leavesC * /
class MainActivity : AppCompatActivity() {

    private val view by lazy {
        findViewById<View>(R.id.view)
    }

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        getWidthHeight("onCreate")
        view.post {
            getWidthHeight("view.Post")
        }
        Handler().post {
            getWidthHeight("handler")}}override fun onResume(a) {
        super.onResume()
        getWidthHeight("onResume")}private fun getWidthHeight(tag: String) {
        Log.e(tag, "width: " + view.width)
        Log.e(tag, "height: " + view.height)
    }

}
Copy the code
github.leavesc.view E/onCreate: width: 0
github.leavesc.view E/onCreate: height: 0
github.leavesc.view E/onResume: width: 0
github.leavesc.view E/onResume: height: 0
github.leavesc.view E/handler: width: 0
github.leavesc.view E/handler: height: 0
github.leavesc.view E/view.Post: width: 263
github.leavesc.view E/view.Post: height: 263
Copy the code

From this, several questions can be raised:

  • View.post(Runnable)Why can I get the true width and height of a View
  • Handler.post(Runnable)andView.post(Runnable)What’s the difference?
  • inonCreate,onResumeWhy can’t we get the true width and height of the View directly from the function
  • View.post(Runnable)Who executes the Runnable in, and can you guarantee that it will be executed

The following will answer these questions one by one, this article based on Android API 30 analysis

A, the post (Runnable)

View. Post (Runnable) method signature, we can see that Runnable processing logic is divided into two types:

  • ifmAttachInfoIf the value is not null, Runnable is assignedmAttachInfoInternal Handler for processing
  • ifmAttachInfoIf null, the Runnable is assigned to the HandlerActionQueue for processing
    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;
    }

    private HandlerActionQueue getRunQueue(a) {
        if (mRunQueue == null) {
            mRunQueue = new HandlerActionQueue();
        }
        return mRunQueue;
    }
Copy the code

1, AttachInfo

Let’s look at the first processing logic of view.post (Runnable)

AttachInfo is a static class inside the View that holds a Handler object, provided by ViewRootImpl, as you can see from the comments

final static class AttachInfo {
    
        /**
         * A Handler supplied by a view's {@link android.view.ViewRootImpl}. This
         * handler can be used to pump events in the UI events queue.
         */
        @UnsupportedAppUsage
        finalHandler mHandler; AttachInfo(IWindowSession session, IWindow window, Display display, ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer, Context Context) {路路路 mHandler = handler; ......}}Copy the code

Finding mAttachInfo’s assignment time can be traced to the View’s dispatchAttachedToWindow method, which means that the View has been attached to the Window

	@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    void dispatchAttachedToWindow(AttachInfo info, int visibility) { mAttachInfo = info; ...}Copy the code

When to call the dispatchAttachedToWindow method, you can trace to the ViewRootImpl class. The ViewRootImpl contains a Handler object, mHandler, and initializes mAttachInfo with mHandler as one of the construction parameters in the constructor. The performTraversals() method of ViewRootImpl calls the DecorView dispatchAttachedToWindow method and passes it to mAttachInfo, The dispatchAttachedToWindow method of all views in the entire View tree is called layer by layer, so that all ChildViews can get the mAttachInfo object

	final ViewRootHandler mHandler = new ViewRootHandler();

    public ViewRootImpl(Context context, Display display, IWindowSession session,
                        boolean useSfChoreographer) {... mAttachInfo =new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context); ...}private void performTraversals(a) {...if(mFirst) {... host. DispatchAttachedToWindow (mAttachInfo,0); 路路路 路路路 路路路 路路路 路路路 路路路 路路路 路路路 路路路 路 performLayout(lp, mWidth, mHeight); performDraw(); ...}Copy the code

In addition, the performLayout () method is responsible for starting the Measure, Layout, and Draw traversals of the whole View tree, and the View can only determine its width and height when performLayout is called. PerformTraversals () itself is called by the ViewRootHandler, that is, the rendering task of the entire view tree is inserted into the MessageQueue and then taken out by the main thread for execution. Since messages inserted into MessageQueue are executed sequentially by the main thread, attachInfo.mHandler.post(Action) ensures that the action will not be invoked until performTraversals have been executed. So we can get the true width and height of the View in Runnable

2, HandlerActionQueue

View. Post (Runnable) is the second processing logic

The HandlerActionQueue can be thought of as a task queue dedicated to storing Runnable tasks, while mActions stores all Runnable tasks to be executed and the corresponding latency. The two POST methods are used to save Runnable objects to be executed into mActions, and executeActions is responsible for submitting all tasks in mActions to the Handler for execution

public class HandlerActionQueue {
    
    private HandlerAction[] mActions;
    private int mCount;

    public void post(Runnable action) {
        postDelayed(action, 0);
    }

    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++; }}public void executeActions(Handler handler) {
        synchronized (this) {
            final HandlerAction[] actions = mActions;
            for (int i = 0, count = mCount; i < count; i++) {
                final HandlerAction handlerAction = actions[i];
                handler.postDelayed(handlerAction.action, handlerAction.delay);
            }

            mActions = null;
            mCount = 0; }}private static class HandlerAction {
        final Runnable action;
        final long delay;

        public HandlerAction(Runnable action, long delay) {
            this.action = action;
            this.delay = delay;
        }

        public boolean matches(Runnable otherAction) {
            return otherAction == null && action == null|| action ! =null&& action.equals(otherAction); ...}}}Copy the code

So, getRunQueue().post(action) just saves our submitted Runnable object into mActions, and requires an external initiative to call the executeActions method to perform the task

The active execution of the task is also done by the View’s dispatchAttachedToWindow so that all tasks in the mActions are inserted into the mHandler’s MessageQueue. MActions will be executed after the main thread has executed the performTraversals() method, so we can still get the View’s true width and height

	@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    void dispatchAttachedToWindow(AttachInfo info, int visibility) { mAttachInfo = info; ...// Transfer all pending runnables.
        if(mRunQueue ! =null) {
            mRunQueue.executeActions(info.mHandler);
            mRunQueue = null; }...}Copy the code

Second, the Handler. Post (Runnable)

What’s the difference between handler. post(Runnable) and view. post(Runnable)?

View.post(Runnable) can obtain the true width and height of the View, mainly because it ensures that the operation to obtain the width and height of the View must be executed after the View is drawn. Handler.post(Runnable) does not work because it does not guarantee this

Both post(Runnable) operations insert tasks into the same MessageQueue and are ultimately executed by the main thread. However, the task of drawing the View tree is submitted after onResume is called back, so the task submitted by the Handler in onCreate will be executed before the task of drawing the View tree, and therefore will not be able to get the true width and height of the View

OnCreate & onResume

Why can’t the onCreate and onResume functions also get the true width and height of the View?

When performTraversals() of ViewRootImpl is not executed when onCreate and onResume are called back.

This can be found in the call chain ActivityThread -> WindowManagerImpl -> WindowManagerGlobal -> ViewRootImpl

First, the ActivityThread’s handleResumeActivity method is responsible for calling the Activity’s onResume method back and forth, and if the Activity is being started for the first time, The DecorView is added to the ViewManager (WM)

	@Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {...// The Activity's onResume method
        finalActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason); ...if (r.window == null&&! ViewManager wm = a.getwinDowManager (); ViewManager wm = a.getwinDowManager ();if (a.mVisibleFromClient) {
                if(! a.mWindowAdded) { a.mWindowAdded =true;
                    / / the key
                    wm.addView(decor, l);
                } else{ a.onWindowAttributesChanged(l); }}}else if(! willBeVisible) {if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
            r.hideForNow = true; }...}Copy the code

The concrete implementation class of the ViewManager here is WindowManagerImpl, which passes the operation to Windows ManagerGlobal

    @UnsupportedAppUsage
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

	@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
                mContext.getUserId());
    }
Copy the code

WindowManagerGlobal will initialize the ViewRootImpl and call its setView method, which will then call the performTraversals method inside to start the view tree drawing process

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow, int userId) {... ViewRootImpl root; View panelParentView =null;
        synchronized(mLock) {路路路 root =new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
            // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView, userId);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throwe; }}}Copy the code

So performTraversals is called after onResume, so we can’t get the View from onCreate or onResume. Of course, the width and height properties of the View are naturally obtained when the Activity calls the onResume method for the second time in a single lifecycle

View. Post (Runnable) compatibility

A conclusion can be drawn from the above analysis: Since view.post (Runnable) ultimately inserts tasks into the MessageQueue associated with the main thread and is ultimately executed sequentially by the main thread, even if we call view.post (Runnable) from a child thread, we can still get the View’s correct width and height

The view.post (Runnable) method also has a version-compatibility issue and is implemented differently on API 23 and earlier

	//Android API 24 and later
	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;
    }

	//Android API 23 and earlier
	public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if(attachInfo ! =null) {
            return attachInfo.mHandler.post(action);
        }
        // Assume that post will succeed later
        ViewRootImpl.getRunQueue().post(action);
        return true;
    }
Copy the code

On Android API 23 and earlier, Runnable is stored in sRunQueues, a static member variable inside ViewRootImpl, when attachInfo is null. Internally, runqueues are held in ThreadLocal queues, which means that different threads get runqueues from different objects. This also means that if we call view.post (Runnable) in child threads, The Runnable is never executed because the primary thread cannot get the RunQueue of the child thread at all

    static final ThreadLocal<RunQueue> sRunQueues = new ThreadLocal<RunQueue>();

	static RunQueue getRunQueue(a) {
        RunQueue rq = sRunQueues.get();
        if(rq ! =null) {
            return rq;
        }
        rq = new RunQueue();
        sRunQueues.set(rq);
        return rq;
    }
Copy the code

Also, since sRunQueues are static member variables, the queues always correspond to the same RunQueue object. If we had called view.post (Runnable) on the queues, The Runnable is then added to the RunQueue associated with the main thread, which then fetches the Runnable for execution

Even if the View is an object that we created directly (as in the example below), this is still valid. When the system needs to draw another View, the task will be pulled out, which is usually done very quickly. Of course, since the View has no attachedToWindow at this point, the width and height value must also be 0

        val view = View(Context)
        view.post {
            getWidthHeight("view.Post")}Copy the code

The view.post (Runnable) method is compatible with the view.post (Runnable) method.

  • When API < 24, if the call is made from the main thread, the submitted Runnable will be executed regardless of whether the View has AttachedToWindow. But you can only get the View’s true width and height if the View is AttachedToWindow
  • When API < 24, if the call is made in a child thread, the submitted Runnable will never be executed, regardless of whether the View has AttachedToWindow
  • When API >= 24, whenever the View is AttachedToWindow, the Runnable submitted will be executed and the View’s true width and height will be retrieved. Runnable will never be executed if it is not AttachedToWindow

Five, the summary

After the Activity’s onResume method is called for the first time, the Runnable that draws the view tree is posted to the MessageQueue associated with the main thread. Although both the Runnable and the onResume method of the callback Activity are performed on the main thread, the Runnable will not be executed until the main thread retrieves it from MessageQueue, so they constitute asynchronous behavior. That’s why we can’t get the width and height of the View directly from onCreate and onResume

When the View is not mapped, through the post (Runnable) submitted by the Runnable will wait until the dispatchAttachedToWindow method is invoked after will be saved to the MessageQueue, This also ensures that the Runnable will not be executed until the View is drawn, so that we can get the View’s width and height

In addition to view. post(Runnable), we can also use OnGlobalLayoutListener to get the View’s width and height properties. The onGlobalLayout method is called when the View tree changes. In this method we can get the width and height of the View

        view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
            override fun onGlobalLayout(a) {
                view.viewTreeObserver.removeOnGlobalLayoutListener(this)
                val width = view.width
            }
        })
Copy the code

In my opinion, the View. Post (Runnable) method is not only used to obtain the width and height properties of the View, but also to provide an optimization method, so that we can perform some operations that are not urgent but must be performed after the entire View tree is drawn. The entire view tree can be rendered as quickly as possible to optimize the user experience