国产成人精品久久免费动漫-国产成人精品天堂-国产成人精品区在线观看-国产成人精品日本-a级毛片无码免费真人-a级毛片毛片免费观看久潮喷

您的位置:首頁技術文章
文章詳情頁

Android View.Post 的原理及缺陷

瀏覽:3日期:2022-09-20 14:20:57

很多開發者都了解這么一個知識點:在 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的資料請關注好吧啦網其它相關文章!

標簽: Android
相關文章:
主站蜘蛛池模板: 国产真真人女人特级毛片 | 多人伦精品一区二区三区视频 | 欧美韩国xxx | 欧美aaaaa一级毛片在线 | 欧美 另类 精品一区视频 | 久久精品免视着国产成人 | 99热精品在线观看 | 免费看美女毛片 | 久久丁香| 2017天天爽夜夜爽精品视频 | 久久中文字幕日韩精品 | 色色视频免费网 | se就是色94欧美setu | 久艹在线观看视频 | 日韩午夜免费视频 | 中国三级毛片 | 美女网站免费观看视频 | 国产成人亚洲综合 | 成人免费久久精品国产片久久影院 | 成人网18免费软件大全 | 国产女人伦码一区二区三区不卡 | 成人黄色一级视频 | 亚洲免费视 | 久草草视频在线观看免费高清 | 欧美高清亚洲欧美一区h | 国产成人在线播放视频 | 女人张开腿 让男人桶视频 女人张开腿等男人桶免费视频 | 日本加勒比高清一本大道 | 成年人午夜网站 | www.亚洲天堂.com | 日本欧美韩国一区二区三区 | 美女视频永久黄网站在线观看 | 欧美成人毛片免费网站 | 国产a级高清版毛片 | 成年女人毛片免费视频 | 久久免费高清视频 | 精品国产理论在线观看不卡 | 91高清国产经典在线观看 | 久久综合久久88 | 国产一区二区三区在线看 | 96精品免费视频大全 |