前言
為什么view.post()能保證獲取到view的寬高?本文將手把手帶你深入源碼了解view.post() 原理。
背景
- 業務需求代碼開始時機一般是在:Activity的生命周期onCreate()
- 視圖View 繪制時機:Activity的生命周期onResume()之后
(注:ActivityThread 的 handleResumeActivity()執行順序:先回調 Activity 生命周期 onResume() - 再開始 View 的繪制任務)
矛盾
- 業務需求代碼需獲取寬高的時機 跟 View的繪制時機 存在時序問題
- 一般來說,業務需求代碼開始時就需要獲取View的相關信息(如寬、高),但:View 繪制時機在Activity.onResume()之后,即在Activity.onCreate()之后 = 業務需求代碼開始后。
解決方案
將需執行的任務傳入到View.post() 。這個操作大家一定很熟悉。那么 其內部原理是什么呢?
結論
以Handler為基礎,View.post() 將傳入任務的執行時機調整到View 繪制完成之后。下面我將從源碼的角度進行分析。
源碼解析
我們直接從view.post()入手:
public boolean post(Runnable action) {
// 僅貼出關鍵代碼
// ...
// 判斷AttachInfo是否為null
final AttachInfo attachInfo = mAttachInfo;
// 過程1:若不為null,直接調用其內部Handler的post
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// 過程2:若為null,則加入當前View的等待隊列
getRunQueue().post(action);
// getRunQueue() 返回的是 HandlerActionQueue
// action代表傳入的要執行的任務
// 即調用了HandlerActionQueue.post() ->> 分析1
return true;
}
此處分成兩個過程講解:
- 當AttachInfo不為null時,直接調用其內部Handler的post;
- 當AttachInfo為null時,則將任務加入當前View的等待隊列中。
此處為了方便理解,我會先講解過程2
過程2:當AttachInfo為null時,則將任務加入當前View的等待隊列中。
public boolean post(Runnable action) {
// 僅貼出關鍵代碼
// ...
// 判斷AttachInfo是否為null
final AttachInfo attachInfo = mAttachInfo;
// 過程1:若不為null,直接調用其內部Handler的post
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// 過程2:若為null,則加入當前View的等待隊列
getRunQueue().post(action);
// getRunQueue() 返回的是 HandlerActionQueue
// action代表傳入的要執行的任務
// 即調用了HandlerActionQueue.post() ->> 分析1
return true;
}
/**
* 分析1:HandlerActionQueue.post()
*/
public void post(Runnable action) {
// ...
postDelayed(action, 0);
// ->> 分析2
}
/**
* 分析2
*/
public void postDelayed(Runnable action, long delayMillis) {
// 1. 將傳入的任務runnable封裝成HandlerAction ->>分析3
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
synchronized (this) {
// 2. 將要執行的HandlerAction 保存在 mActions 數組中
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}
/**
* 分析3:HandlerAction 表示一個待執行的任務
* 內部持有要執行的 Runnable 和延遲時間
*/
private static class HandlerAction {
// post的任務
final Runnable action;
// 延遲時間
final long delay;
public HandlerAction(Runnable action, long delay) {
this.action = action;
this.delay = delay;
}
// ...
}
// 回到分析原處
結論:
- 將傳入的任務封裝成HandlerAction對象
- 創建一個默認長度為4的 HandlerAction數組,用于保存通過post()添加的任務
注:此時只是保存了通過post()添加的任務,并沒執行。
過程1:當AttachInfo不為null時,直接調用其內部Handler的post()
public boolean post(Runnable action) {
// ...
// 判斷AttachInfo是否為null
final AttachInfo attachInfo = mAttachInfo;
// 過程1:若不為null,直接調用其內部Handler的post ->>分析1
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// 過程2:若為null,則加入當前View的等待隊列
getRunQueue().post(action);
return true;
}
/**
* 分析1:AttachInfo的賦值過程 -> dispatchAttachedToWindow()
* 注:AttachInfo持有當前渲染線程Handler
*/
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
// ...
// 給當前View賦值AttachInfo,此時同一個ViewRootImpl內的所有View共用同一個AttachInfo
mAttachInfo = info;
// mRunQueue,即 前面說的 HandlerActionQueue
// 其內部保存了當前View.post的任務
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
// 執行使用View.post的任務,post到渲染線程的Handler中
// ->> 分析2
}
// 在Activity的onResume()中調用,但是在View繪制流程之前
onAttachedToWindow();
ListenerInfo li = mListenerInfo;
final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
li != null ? li.mOnAttachStateChangeListeners : null;
if (listeners != null && listeners.size() > 0) {
for (OnAttachStateChangeListener listener : listeners) {
// 通知所有監聽View已經onAttachToWindow的客戶端,即view.addOnAttachStateChangeListener();
// 但此時View還沒有開始繪制,不能正確獲取測量大小或View實際大小
listener.onViewAttachedToWindow(this);
}
}
// ...
}
/**
* 分析2:HandlerActionQueue.executeActions()
*/
public void executeActions(Handler handler) {
synchronized (this) {
final HandlerAction[] actions = mActions;
// 任務隊列
// mActions即為前面過程1 保存了通過post()添加的任務 的數組
// 遍歷所有任務
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
// 發送到Handler中,等待執行
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
// 此時不再需要后續的post,將被添加到AttachInfo中
mActions = null;
mCount = 0;
}
}
// ->> 回到分析原處
結論
在AttachInfo被賦值時(即不為null),就會遍歷 前面過程1保存了通過post()添加的任務 的數組,將每個任務發送到handler中等待執行。
下面,我們繼續看,AttachInfo的賦值過程 -> dispatchAttachedToWindow()是什么時候被調用的。
答:dispatchAttachedToWindow()調用時機是在 View 繪制流程的開始階段,即 ViewRootImpl.performTraversals()
/**
* 基礎:
* 1. AttachInfo的創建是在ViewRootImpl的構造方法中
* 2. 同一個 View Hierachy 樹結構中所有View共用一個AttachInfo
*/
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,context);
/**
* dispatchAttachedToWindow()調用時機 = View繪制流程開始
* 即ViewRootImpl.performTraversals()
*/
private void performTraversals() {
// ...
// mView是DecorView
// host的類型是 DecorView(繼承自 FrameLayout)
// 每個Activity都有一個關聯的 Window(當前窗口),每個窗口內部又包含一個 DecorView 對象(描述窗口的xml視圖布局)
final View host = mView;
// 調用DecorVIew的dispatchAttachedToWindow()
// ->> 分析1
host.dispatchAttachedToWindow(mAttachInfo, 0);
getRunQueue().executeActions(mAttachInfo.mHandler);
// 開始View繪制流程的測量、布局、繪制階段
performMeasure();
performLayout();
performDraw();
...
}
/**
* 分析1:DecorVIew.dispatchAttachedToWindow()
* 注:DecorView并無重寫該方法,而是在其父類ViewGroup里
*/
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
super.dispatchAttachedToWindow(info, visibility);
// 子View的數量
final int count = mChildrenCount;
final View[] children = mChildren;
// 遍歷所有子View,調用所有子View的dispatchAttachedToWindow() & 為每個子View關聯AttachInfo
// 子View 的 dispatchAttachedToWindow()在前面過程1已經分析過:
// 即 遍歷 前面過程1保存了通過post()添加的任務 的數組,將每個任務發送到handler中等待執行。
for (int i = 0; i < count; i++) {
final View child = children[i];
child.dispatchAttachedToWindow(info,combineVisibility(visibility, child.getVisibility()));
}
// ...
}
結論
- 通過View.post()添加的任務是在View繪制任務里 - 開始繪制階段時添加到消息隊列的尾部的;
- 所以,View.post() 添加的任務的執行是在View繪制任務后才執行,即在View繪制流程結束之后執行
- 即View.post() 添加的任務能夠保證在所有 View繪制流程結束之后才被執行,所以 執行View.post() 添加的任務時可以正確獲取到 View 的寬高。
額外延伸
a. 問題描述
若只是創建一個 View & 調用它的post(),那么post的任務會不會被執行?
final View view = new View(this);
view.post(new Runnable() {
@Override
public void run() {
// ...
}
});
b. 答案
不會。主要原因是:
每個View中post() 需執行的任務,必須得添加到窗口視圖-執行繪制流程 - 任務才會被post到消息隊列里去等待執行,即依賴于dispatchAttachedToWindow ();
若View未添加到窗口視圖,那么就不會走繪制流程,post() 添加的任務最終不會被post到消息隊列里,即得不到執行。(但會保存到HandlerAction數組里)
上述例子,因為它沒有被添加到窗口視圖,所以不會走繪制流程,所以該任務最終不會被post到消息隊列里 & 執行
c. 解決方案
此時只需要添加將View添加到窗口,那么post()的任務即可被執行
// 因為此時會重新發起繪制流程,post的任務會被放到消息隊列里,所以會被執行
contentView.addView(view);
至此,關于view.post()原理講解完畢
總結
View.post()的原理:以Handler為基礎,View.post() 將傳入任務添加到 View繪制任務所在的消息隊列尾部,從而保證View.post() 任務的執行時機是在View 繪制任務完成之后的。 其中,幾個關鍵點:
1-View.post()實際操作:將view.post()傳入的任務保存到一個數組里 /
2-View.post()添加的任務 添加到 View繪制任務所在的消息隊列尾部的時機:View 繪制流程的開始階段,即 ViewRootImpl.performTraversals()
3-View.post()添加的任務執行時機:在View繪制任務之后
接下來推出的文章,我將繼續講解Android
的相關知識,感興趣的讀者可以繼續關注我的博客哦:Carson_Ho的Android博客
相關系列文章閱讀
Carson帶你學Android:學習方法
Carson帶你學Android:四大組件
Carson帶你學Android:自定義View
Carson帶你學Android:異步-多線程
Carson帶你學Android:性能優化
Carson帶你學Android:動畫
歡迎關注Carson_Ho的簡書
不定期分享關于安卓開發的干貨,追求短、平、快,但卻不缺深度。