Android View.Post 的原理及缺陷
很多開發者都了解這么一個知識點:在 Activity 的 onCreate 方法里我們無法直接獲取到 View 的寬高信息,但通過 View.post(Runnable)這種方式就可以,那背后的具體原因你是否有了解過呢?
讀者可以嘗試以下操作。可以發現,除了通過 View.post(Runnable)這種方式可以獲得 View 的真實寬高外,其它方式取得的值都是 0
/** * 作者:leavesC * 時間:2020/03/14 11:05 * 描述: * 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() { super.onResume() getWidthHeight('onResume') } private fun getWidthHeight(tag: String) { Log.e(tag, 'width: ' + view.width) Log.e(tag, 'height: ' + view.height) }}
github.leavesc.view E/onCreate: width: 0github.leavesc.view E/onCreate: height: 0github.leavesc.view E/onResume: width: 0github.leavesc.view E/onResume: height: 0github.leavesc.view E/handler: width: 0github.leavesc.view E/handler: height: 0github.leavesc.view E/view.Post: width: 263github.leavesc.view E/view.Post: height: 263
從這就可以引申出幾個疑問:
View.post(Runnable) 為什么可以得到 View 的真實寬高 Handler.post(Runnable)和View.post(Runnable)有什么區別 在 onCreate、onResume 函數中為什么無法直接得到 View 的真實寬高 View.post(Runnable) 中的 Runnable 是由誰來執行的,可以保證一定會被執行嗎后邊就來一一解答這幾個疑問,本文基于 Android API 30 進行分析
一、View.post(Runnable)看下 View.post(Runnable) 的方法簽名,可以看出 Runnable 的處理邏輯分為兩種:
如果 mAttachInfo 不為 null,則將 Runnable 交由mAttachInfo內部的 Handler 進行處理 如果 mAttachInfo 為 null,則將 Runnable 交由 HandlerActionQueue 進行處理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() { if (mRunQueue == null) { mRunQueue = new HandlerActionQueue(); } return mRunQueue; }1、AttachInfo
先來看View.post(Runnable)的第一種處理邏輯
AttachInfo 是 View 內部的一個靜態類,其內部持有一個 Handler 對象,從注釋可知它是由 ViewRootImpl 提供的
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 final Handler mHandler; AttachInfo(IWindowSession session, IWindow window, Display display, ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer, Context context) { ··· mHandler = handler; ··· } ···}
查找 mAttachInfo 的賦值時機可以追蹤到 View 的 dispatchAttachedToWindow 方法,該方法被調用就意味著 View 已經 Attach 到 Window 上了
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) void dispatchAttachedToWindow(AttachInfo info, int visibility) { mAttachInfo = info; ··· }
再查找dispatchAttachedToWindow 方法的調用時機,可以跟蹤到 ViewRootImpl 類。ViewRootImpl 內就包含一個 Handler 對象 mHandler,并在構造函數中以 mHandler 作為構造參數之一來初始化 mAttachInfo。ViewRootImpl 的performTraversals()方法就會調用 DecorView 的 dispatchAttachedToWindow 方法并傳入 mAttachInfo,從而層層調用整個視圖樹中所有 View 的 dispatchAttachedToWindow 方法,使得所有 childView 都能獲取到 mAttachInfo 對象
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() { ··· if (mFirst) { ··· host.dispatchAttachedToWindow(mAttachInfo, 0); ··· } ··· performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); performLayout(lp, mWidth, mHeight); performDraw(); ··· }
此外,performTraversals()方法也負責啟動整個視圖樹的 Measure、Layout、Draw 流程,只有當 performLayout 被調用后 View 才能確定自己的寬高信息。而 performTraversals()本身也是交由 ViewRootHandler 來調用的,即整個視圖樹的繪制任務也是先插入到 MessageQueue 中,后續再由主線程取出任務進行執行。由于插入到 MessageQueue 中的消息是交由主線程來順序執行的,所以 attachInfo.mHandler.post(action)就保證了 action 一定是在 performTraversals 執行完畢后才會被調用,因此我們就可以在 Runnable 中獲取到 View 的真實寬高了
2、HandlerActionQueue再來看View.post(Runnable)的第二種處理邏輯
HandlerActionQueue 可以看做是一個專門用于存儲 Runnable 的任務隊列,mActions 就存儲了所有要執行的 Runnable 和相應的延時時間。兩個post方法就用于將要執行的 Runnable 對象保存到 mActions中,executeActions就負責將mActions中的所有任務提交給 Handler 執行
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); } } ··· }
所以說,getRunQueue().post(action)只是將我們提交的 Runnable 對象保存到了 mActions 中,還需要外部主動調用 executeActions方法來執行任務
而這個主動執行任務的操作也是由 View 的 dispatchAttachedToWindow來完成的,從而使得 mActions 中的所有任務都會被插入到 mHandler 的 MessageQueue 中,等到主線程執行完 performTraversals() 方法后就會來執行 mActions,所以此時我們依然可以獲取到 View 的真實寬高
@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; } ··· }二、Handler.post(Runnable)
Handler.post(Runnable)和View.post(Runnable)有什么區別呢?
從上面的源碼分析就可以知道,View.post(Runnable)之所以可以獲取到 View 的真實寬高,主要就是因為確保了獲取 View 寬高的操作一定是在 View 繪制完畢之后才被執行,而 Handler.post(Runnable)之所以不行,就是其無法保證這一點
雖然這兩種post(Runnable)的操作都是往同個 MessageQueue 插入任務,且最終都是交由主線程來執行。但繪制視圖樹的任務是在onResume被回調后才被提交的,所以我們在onCreate中用 Handler 提交的任務就會早于繪制視圖樹的任務被執行,因此也就無法獲取到 View 的真實寬高了
三、onCreate & onResume在 onCreate、onResume 函數中為什么無法也直接得到 View 的真實寬高呢?
從結果反推原因,這說明當 onCreate、onResume被回調時 ViewRootImpl 的 performTraversals()方法還未執行,那么performTraversals()方法的具體執行時機是什么時候呢?
這可以從 ActivityThread -> WindowManagerImpl -> WindowManagerGlobal -> ViewRootImpl 這條調用鏈上找到答案
首先,ActivityThread 的 handleResumeActivity 方法就負責來回調 Activity 的 onResume 方法,且如果當前 Activity 是第一次啟動,則會向 ViewManager(wm)添加 DecorView
@Override public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) { ··· //Activity 的 onResume 方法 final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason); ··· if (r.window == null && !a.mFinished && willBeVisible) { ··· ViewManager wm = a.getWindowManager(); if (a.mVisibleFromClient) { if (!a.mWindowAdded) { a.mWindowAdded = true; //重點 wm.addView(decor, l); } else { a.onWindowAttributesChanged(l); } } } else if (!willBeVisible) { if (localLOGV) Slog.v(TAG, 'Launch ' + r + ' mStartedActivity set'); r.hideForNow = true; }··· }
此處的 ViewManager 的具體實現類即 WindowManagerImpl,WindowManagerImpl 會將操作轉交給 WindowManagerGlobal
@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()); }
WindowManagerGlobal 就會完成 ViewRootImpl 的初始化并且調用其 setView 方法,該方法內部就會再去調用 performTraversals 方法啟動視圖樹的繪制流程
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); } throw e; } } }
所以說, performTraversals 方法的調用時機是在 onResume 方法之后,所以我們在 onCreate和onResume 函數中都無法獲取到 View 的實際寬高。當然,當 Activity 在單次生命周期過程中第二次調用onResume 方法時自然就可以獲取到 View 的寬高屬性
四、View.post(Runnable) 的兼容性從以上分析可以得出一個結論:由于 View.post(Runnable)最終都是往和主線程關聯的 MessageQueue 中插入任務且最終由主線程來順序執行,所以即使我們是在子線程中調用View.post(Runnable),最終也可以得到 View 正確的寬高值
但該結論也只在 API 24 及之后的版本上才成立,View.post(Runnable) 方法也存在著一個版本兼容性問題,在 API 23 及之前的版本上有著不同的實現方式
//Android API 24 及之后的版本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 及之前的版本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; }
在 Android API 23 及之前的版本上,當 attachInfo 為 null 時,會將 Runnable 保存到 ViewRootImpl 內部的一個靜態成員變量 sRunQueues 中。而 sRunQueues 內部是通過 ThreadLocal 來保存 RunQueue 的,這意味著不同線程獲取到的 RunQueue 是不同對象,這也意味著如果我們在子線程中調用View.post(Runnable) 方法的話,該 Runnable 永遠不會被執行,因為主線程根本無法獲取到子線程的 RunQueue
static final ThreadLocal<RunQueue> sRunQueues = new ThreadLocal<RunQueue>();static RunQueue getRunQueue() { RunQueue rq = sRunQueues.get(); if (rq != null) { return rq; } rq = new RunQueue(); sRunQueues.set(rq); return rq; }
此外,由于sRunQueues 是靜態成員變量,主線程會一直對應同一個 RunQueue 對象,如果我們是在主線程中調用View.post(Runnable)方法的話,那么該 Runnable 就會被添加到和主線程關聯的 RunQueue 中,后續主線程就會取出該 Runnable 來執行
即使該 View 是我們直接 new 出來的對象(就像以下的示例),以上結論依然生效,當系統需要繪制其它視圖的時候就會順便取出該任務,一般很快就會執行到。當然,由于此時 View 并沒有 AttachedToWindow,所以獲取到的寬高值肯定也是 0
val view = View(Context) view.post { getWidthHeight('view.Post') }
對View.post(Runnable)方法的兼容性問題做下總結:
當 API < 24 時,如果是在主線程進行調用,那么不管 View 是否有 AttachedToWindow,提交的 Runnable 均會被執行。但只有在 View 被 AttachedToWindow 的情況下才可以獲取到 View 的真實寬高 當 API < 24 時,如果是在子線程進行調用,那么不管 View 是否有 AttachedToWindow,提交的 Runnable 都將永遠不會被執行 當 API >= 24 時,不管是在主線程還是子線程進行調用,只要 View 被 AttachedToWindow 后,提交的 Runnable 都會被執行,且都可以獲取到 View 的真實寬高值。如果沒有被 AttachedToWindow 的話,Runnable 也將永遠不會被執行以上就是Android View.Post 的原理及缺陷的詳細內容,更多關于Android View.Post的資料請關注好吧啦網其它相關文章!
相關文章:
