Android View.Post 的原理及缺陷
很多開(kāi)發(fā)者都了解這么一個(gè)知識(shí)點(diǎn):在 Activity 的 onCreate 方法里我們無(wú)法直接獲取到 View 的寬高信息,但通過(guò) View.post(Runnable)這種方式就可以,那背后的具體原因你是否有了解過(guò)呢?
讀者可以嘗試以下操作??梢园l(fā)現(xiàn),除了通過(guò) View.post(Runnable)這種方式可以獲得 View 的真實(shí)寬高外,其它方式取得的值都是 0
/** * 作者:leavesC * 時(shí)間: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
從這就可以引申出幾個(gè)疑問(wèn):
View.post(Runnable) 為什么可以得到 View 的真實(shí)寬高 Handler.post(Runnable)和View.post(Runnable)有什么區(qū)別 在 onCreate、onResume 函數(shù)中為什么無(wú)法直接得到 View 的真實(shí)寬高 View.post(Runnable) 中的 Runnable 是由誰(shuí)來(lái)執(zhí)行的,可以保證一定會(huì)被執(zhí)行嗎后邊就來(lái)一一解答這幾個(gè)疑問(wèn),本文基于 Android API 30 進(jìn)行分析
一、View.post(Runnable)看下 View.post(Runnable) 的方法簽名,可以看出 Runnable 的處理邏輯分為兩種:
如果 mAttachInfo 不為 null,則將 Runnable 交由mAttachInfo內(nèi)部的 Handler 進(jìn)行處理 如果 mAttachInfo 為 null,則將 Runnable 交由 HandlerActionQueue 進(jìn)行處理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
先來(lái)看View.post(Runnable)的第一種處理邏輯
AttachInfo 是 View 內(nèi)部的一個(gè)靜態(tài)類,其內(nèi)部持有一個(gè) Handler 對(duì)象,從注釋可知它是由 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 的賦值時(shí)機(jī)可以追蹤到 View 的 dispatchAttachedToWindow 方法,該方法被調(diào)用就意味著 View 已經(jīng) Attach 到 Window 上了
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) void dispatchAttachedToWindow(AttachInfo info, int visibility) { mAttachInfo = info; ··· }
再查找dispatchAttachedToWindow 方法的調(diào)用時(shí)機(jī),可以跟蹤到 ViewRootImpl 類。ViewRootImpl 內(nèi)就包含一個(gè) Handler 對(duì)象 mHandler,并在構(gòu)造函數(shù)中以 mHandler 作為構(gòu)造參數(shù)之一來(lái)初始化 mAttachInfo。ViewRootImpl 的performTraversals()方法就會(huì)調(diào)用 DecorView 的 dispatchAttachedToWindow 方法并傳入 mAttachInfo,從而層層調(diào)用整個(gè)視圖樹中所有 View 的 dispatchAttachedToWindow 方法,使得所有 childView 都能獲取到 mAttachInfo 對(duì)象
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()方法也負(fù)責(zé)啟動(dòng)整個(gè)視圖樹的 Measure、Layout、Draw 流程,只有當(dāng) performLayout 被調(diào)用后 View 才能確定自己的寬高信息。而 performTraversals()本身也是交由 ViewRootHandler 來(lái)調(diào)用的,即整個(gè)視圖樹的繪制任務(wù)也是先插入到 MessageQueue 中,后續(xù)再由主線程取出任務(wù)進(jìn)行執(zhí)行。由于插入到 MessageQueue 中的消息是交由主線程來(lái)順序執(zhí)行的,所以 attachInfo.mHandler.post(action)就保證了 action 一定是在 performTraversals 執(zhí)行完畢后才會(huì)被調(diào)用,因此我們就可以在 Runnable 中獲取到 View 的真實(shí)寬高了
2、HandlerActionQueue再來(lái)看View.post(Runnable)的第二種處理邏輯
HandlerActionQueue 可以看做是一個(gè)專門用于存儲(chǔ) Runnable 的任務(wù)隊(duì)列,mActions 就存儲(chǔ)了所有要執(zhí)行的 Runnable 和相應(yīng)的延時(shí)時(shí)間。兩個(gè)post方法就用于將要執(zhí)行的 Runnable 對(duì)象保存到 mActions中,executeActions就負(fù)責(zé)將mActions中的所有任務(wù)提交給 Handler 執(zhí)行
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); } } ··· }
所以說(shuō),getRunQueue().post(action)只是將我們提交的 Runnable 對(duì)象保存到了 mActions 中,還需要外部主動(dòng)調(diào)用 executeActions方法來(lái)執(zhí)行任務(wù)
而這個(gè)主動(dòng)執(zhí)行任務(wù)的操作也是由 View 的 dispatchAttachedToWindow來(lái)完成的,從而使得 mActions 中的所有任務(wù)都會(huì)被插入到 mHandler 的 MessageQueue 中,等到主線程執(zhí)行完 performTraversals() 方法后就會(huì)來(lái)執(zhí)行 mActions,所以此時(shí)我們依然可以獲取到 View 的真實(shí)寬高
@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)有什么區(qū)別呢?
從上面的源碼分析就可以知道,View.post(Runnable)之所以可以獲取到 View 的真實(shí)寬高,主要就是因?yàn)榇_保了獲取 View 寬高的操作一定是在 View 繪制完畢之后才被執(zhí)行,而 Handler.post(Runnable)之所以不行,就是其無(wú)法保證這一點(diǎn)
雖然這兩種post(Runnable)的操作都是往同個(gè) MessageQueue 插入任務(wù),且最終都是交由主線程來(lái)執(zhí)行。但繪制視圖樹的任務(wù)是在onResume被回調(diào)后才被提交的,所以我們?cè)趏nCreate中用 Handler 提交的任務(wù)就會(huì)早于繪制視圖樹的任務(wù)被執(zhí)行,因此也就無(wú)法獲取到 View 的真實(shí)寬高了
三、onCreate & onResume在 onCreate、onResume 函數(shù)中為什么無(wú)法也直接得到 View 的真實(shí)寬高呢?
從結(jié)果反推原因,這說(shuō)明當(dāng) onCreate、onResume被回調(diào)時(shí) ViewRootImpl 的 performTraversals()方法還未執(zhí)行,那么performTraversals()方法的具體執(zhí)行時(shí)機(jī)是什么時(shí)候呢?
這可以從 ActivityThread -> WindowManagerImpl -> WindowManagerGlobal -> ViewRootImpl 這條調(diào)用鏈上找到答案
首先,ActivityThread 的 handleResumeActivity 方法就負(fù)責(zé)來(lái)回調(diào) Activity 的 onResume 方法,且如果當(dāng)前 Activity 是第一次啟動(dòng),則會(huì)向 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; //重點(diǎn) wm.addView(decor, l); } else { a.onWindowAttributesChanged(l); } } } else if (!willBeVisible) { if (localLOGV) Slog.v(TAG, 'Launch ' + r + ' mStartedActivity set'); r.hideForNow = true; }··· }
此處的 ViewManager 的具體實(shí)現(xiàn)類即 WindowManagerImpl,WindowManagerImpl 會(huì)將操作轉(zhuǎn)交給 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 就會(huì)完成 ViewRootImpl 的初始化并且調(diào)用其 setView 方法,該方法內(nèi)部就會(huì)再去調(diào)用 performTraversals 方法啟動(dòng)視圖樹的繪制流程
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; } } }
所以說(shuō), performTraversals 方法的調(diào)用時(shí)機(jī)是在 onResume 方法之后,所以我們?cè)?onCreate和onResume 函數(shù)中都無(wú)法獲取到 View 的實(shí)際寬高。當(dāng)然,當(dāng) Activity 在單次生命周期過(guò)程中第二次調(diào)用onResume 方法時(shí)自然就可以獲取到 View 的寬高屬性
四、View.post(Runnable) 的兼容性從以上分析可以得出一個(gè)結(jié)論:由于 View.post(Runnable)最終都是往和主線程關(guān)聯(lián)的 MessageQueue 中插入任務(wù)且最終由主線程來(lái)順序執(zhí)行,所以即使我們是在子線程中調(diào)用View.post(Runnable),最終也可以得到 View 正確的寬高值
但該結(jié)論也只在 API 24 及之后的版本上才成立,View.post(Runnable) 方法也存在著一個(gè)版本兼容性問(wèn)題,在 API 23 及之前的版本上有著不同的實(shí)現(xiàn)方式
//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 及之前的版本上,當(dāng) attachInfo 為 null 時(shí),會(huì)將 Runnable 保存到 ViewRootImpl 內(nèi)部的一個(gè)靜態(tài)成員變量 sRunQueues 中。而 sRunQueues 內(nèi)部是通過(guò) ThreadLocal 來(lái)保存 RunQueue 的,這意味著不同線程獲取到的 RunQueue 是不同對(duì)象,這也意味著如果我們?cè)谧泳€程中調(diào)用View.post(Runnable) 方法的話,該 Runnable 永遠(yuǎn)不會(huì)被執(zhí)行,因?yàn)橹骶€程根本無(wú)法獲取到子線程的 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 是靜態(tài)成員變量,主線程會(huì)一直對(duì)應(yīng)同一個(gè) RunQueue 對(duì)象,如果我們是在主線程中調(diào)用View.post(Runnable)方法的話,那么該 Runnable 就會(huì)被添加到和主線程關(guān)聯(lián)的 RunQueue 中,后續(xù)主線程就會(huì)取出該 Runnable 來(lái)執(zhí)行
即使該 View 是我們直接 new 出來(lái)的對(duì)象(就像以下的示例),以上結(jié)論依然生效,當(dāng)系統(tǒng)需要繪制其它視圖的時(shí)候就會(huì)順便取出該任務(wù),一般很快就會(huì)執(zhí)行到。當(dāng)然,由于此時(shí) View 并沒(méi)有 AttachedToWindow,所以獲取到的寬高值肯定也是 0
val view = View(Context) view.post { getWidthHeight('view.Post') }
對(duì)View.post(Runnable)方法的兼容性問(wèn)題做下總結(jié):
當(dāng) API < 24 時(shí),如果是在主線程進(jìn)行調(diào)用,那么不管 View 是否有 AttachedToWindow,提交的 Runnable 均會(huì)被執(zhí)行。但只有在 View 被 AttachedToWindow 的情況下才可以獲取到 View 的真實(shí)寬高 當(dāng) API < 24 時(shí),如果是在子線程進(jìn)行調(diào)用,那么不管 View 是否有 AttachedToWindow,提交的 Runnable 都將永遠(yuǎn)不會(huì)被執(zhí)行 當(dāng) API >= 24 時(shí),不管是在主線程還是子線程進(jìn)行調(diào)用,只要 View 被 AttachedToWindow 后,提交的 Runnable 都會(huì)被執(zhí)行,且都可以獲取到 View 的真實(shí)寬高值。如果沒(méi)有被 AttachedToWindow 的話,Runnable 也將永遠(yuǎn)不會(huì)被執(zhí)行以上就是Android View.Post 的原理及缺陷的詳細(xì)內(nèi)容,更多關(guān)于Android View.Post的資料請(qǐng)關(guān)注好吧啦網(wǎng)其它相關(guān)文章!
相關(guān)文章:
1. ASP常用日期格式化函數(shù) FormatDate()2. 如何在jsp界面中插入圖片3. jsp+servlet簡(jiǎn)單實(shí)現(xiàn)上傳文件功能(保存目錄改進(jìn))4. 得到XML文檔大小的方法5. XML入門的常見(jiàn)問(wèn)題(二)6. ASP.NET Core實(shí)現(xiàn)中間件的幾種方式7. 在JSP中使用formatNumber控制要顯示的小數(shù)位數(shù)方法8. JavaScrip簡(jiǎn)單數(shù)據(jù)類型隱式轉(zhuǎn)換的實(shí)現(xiàn)9. jsp實(shí)現(xiàn)textarea中的文字保存換行空格存到數(shù)據(jù)庫(kù)的方法10. CSS3實(shí)例分享之多重背景的實(shí)現(xiàn)(Multiple backgrounds)
