從繪製時機深入淺出View.post
最近新接手了一個專案的 B ug 修復任務 ,其中有一個Bug 是在一個頁面有一個 ImageView,ImageView 根據傳入的圖片設定寬度拉滿或者高度拉滿,然後根據 imageView 的大小透過程式碼繪製一個新的 View ,在 onCreate 中繪製總是位置錯亂,放入 onResume 後, 從後臺 重新進入 應用 就可以正常繪製。
1) 最開始以為是 繪製的時機問題
因為放在 onResume 然後重新進入是正常的,因為這個頁面比較頻繁的設定 view 的 LayoutParams ,懷疑是某個 LayoutParams 設定的時機導致繪製的 view 的位置錯亂(這裡其實思路是接近的,但是沒有繼續深入,導致後面走了一些彎路),但是隻放入 onResume 後還是錯誤的位置
2) view 的繪製 演算法 有問題
但是第二次繪製可以正常執行 , 甚至後面在重新建立的乾淨的類中也是正常的繪製
3) 思考兩次繪製過程中發生了什麼
因為這個類頻繁的設定 LayoutParams , 所以開始把注意力放在這上面 , 看了下 LayoutParams 的原始碼
public void setLayoutParams(ViewGroup.LayoutParams params) { if (params == null) { throw new NullPointerException("params == null"); } mLayoutParams = params; requestLayout(); }
看到了requestLayout() ,突然想起來,對啊,修改了 View 大小、位置等資訊會重新繪製,檢視的測量( measure )和佈局( layout )過程需要重新執行才能更新檢視的尺寸。
而 佈局過程的執行是非同步的,系統會在下一個 UI 幀中才會更新檢視的尺寸。因此,如果在修改 LayoutParams 後立即嘗試獲取檢視的新尺寸,很可能會得到舊的尺寸值。 而我們繪製的 View 是建立出來的 , 他的舊尺寸的寬高就是 0 所以導致我們的演算法計算的有問題
為了驗證 , 寫了個測試的步驟
view.postDelayed({ // 重新操作 },500)
當所有佈局設定完成後 , 延遲 500ms , 重新繪製 , 這個時候就是正常的 , 所以問題就是我發現的那樣 , 但是真正的解決方案肯定不應該是這樣 , 太不優雅了
解決方案有兩個
1 、在所有操作執行完成後,執行頂層 View.post ,將繪製 view 的操作加入頂層 View 的最後一個操作,在頂層 View 繪製到最後一步時會自動幫我們執行繪製的步驟
binding.root.post { //操作 }
2、 在所有操作執行完成後,監聽頂層佈局的繪製,當頂層View 繪製完成後,也就意味著真實的寬 / 高出現了
val observer: ViewTreeObserver = binding.root.viewTreeObserver observer.addOnGlobalLayoutListener(object : OnGlobalLayoutListener { override fun onGlobalLayout() { // 移除監聽器,避免重複呼叫 observer.removeOnGlobalLayoutListener(this) // 獲取檢視的新尺寸 // 在這裡可以使用新的尺寸值 } })
我推薦 view .post 的解決方案 ,那麼view.post 到底幹了什麼, 為什麼會執行我們需要的操作呢 , 先來看看原始碼
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; }
在 view.post 中第一個邏輯是 AttachInfo ,AttachInfo 是 View 內部的一個靜態類,其內部持有一個 Handler 物件,從註釋 可以知道 它是由 ViewRootImpl 提供的
inal 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 物件 。
除此之外 ,performTraversals() 方法也負責啟動整個檢視樹的 Measure 、 Layout 、 Draw 流程,只有當 performLayout 被呼叫後 View 才能確定自己的寬高資訊。而 performTraversals() 本身也是交由 ViewRootHandler 來呼叫的 。
即整個檢視樹的繪製任務也是先插入到 MessageQueue 中,後續再由主執行緒取出任務進行執行。由於插入到 MessageQueue 中的訊息是交由主執行緒來順序執行的,所以 attachInfo.mHandler.post(action) 就保證了 action 一定是在 performTraversals 執行完畢後才會被呼叫,因此我們就可以在 Runnable 中獲取到 View 的真實寬高了
再來看看第二個處理邏輯 getRunQueue().post(action);getRunQueue 實際上返回了一個 HandlerActionQueue ,所以本質上就是呼叫了 HandlerActionQueue.post , 讓我們來看看 HandlerActionQueue 吧
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); } } ··· }
HandlerActionQueue 可以看做是一個專門用於儲存 Runnable 的任務佇列, mActions 就儲存了所有要執行的 Runnable 和相應的延時時間。兩個 post 方法就用於將要執行的 Runnable 物件儲存到 mActions 中, executeActions 就負責將 mActions 中的所有任務提交給 Handler 執行
所以說,getRunQueue().post(action) 只是將我們提交的 Runnable 物件儲存到了 mActions 中,還需要外部主動呼叫 executeActions 方法來執行任務
而這個主動執行任務的操作也是由 View 的 dispatchAttachedToWindow 來完成的,從而使得 mActions 中的所有任務都會被插入到 mHandler 的 MessageQueue 中,等到主執行緒執行完 performTraversals() 方法後就會來執行 mActions ,所以此時我們依然可以獲取到 View 的真實寬高
在 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 的寬高屬性 , 也就是文章開頭我遇到的 bug
來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/69917874/viewspace-3003770/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 深入淺出MongoDB複製MongoDB
- 深入淺出Android系列之從ViewToBitmap延伸到View的繪製全過程AndroidView
- 深入淺出FE(十四)深入淺出websocketWeb
- 深入淺出MongoDB複製【經典介紹】MongoDB
- 深入淺出JavaScript執行機制JavaScript
- 深入淺出Lua虛擬機器虛擬機
- android Binder機制深入淺出Android
- 深入淺出Java回撥機制Java
- 深入淺出OAuth 2.0授權機制OAuth
- 深入淺出——MVCMVC
- 深入淺出mongooseGo
- HTTP深入淺出HTTP
- 深入淺出IO
- 深入淺出 RabbitMQMQ
- 深入淺出PromisePromise
- ArrayList 深入淺出
- mysqldump 深入淺出MySql
- 深入淺出decorator
- 深入淺出 ZooKeeper
- 機器學習深入淺出機器學習
- 深入淺出HTTPHTTP
- http 深入淺出HTTP
- 深入淺出 ARCore
- 深入淺出 synchronizedsynchronized
- 深入淺出WebpackWeb
- 深入淺出 blockBloC
- block深入淺出BloC
- 深入淺出計算機組成原理-徐文浩-極客時間計算機
- 深入淺出 Runtime(三):訊息機制
- 深入淺出瀏覽器快取機制瀏覽器快取
- 隨機森林演算法深入淺出隨機森林演算法
- 深入淺出: Java回撥機制(非同步)Java非同步
- 淺讀-《深入淺出Nodejs》NodeJS
- 元組關係演算(從集合的角度深入淺出)
- Promise拆解計劃:由規範深入,從原理淺出Promise
- RecyclerView 原始碼深入解析——繪製流程、快取機制、動畫等View原始碼快取動畫
- 《深入淺出webpack》有感Web
- 深入淺出 Laravel MacroableLaravelMac