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 ViewHandler.post(Runnable)
andView.post(Runnable)
What’s the difference?- in
onCreate
,onResume
Why 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:
- if
mAttachInfo
If the value is not null, Runnable is assignedmAttachInfo
Internal Handler for processing - if
mAttachInfo
If 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