前言
最近因為個人原因,導致要離開杭州了,人生就是一次又一次的意外,你不知道突然會發生什麼,你能做到的只有把握好每一次機會
原理猜測
在Android中,想要獲取View的寬高,常見的基本都是接呼叫View.getWidth或者View.getMeasuredWidth方法來獲取寬高,但是往往在oncreate或者onresume中,得到的值都是0,也就可以理解為此時View的繪製流程並沒有執行完,熟悉Activity啟動流程的朋友都知道,Activity的生命週期和頁面的繪製流程並不是一個序列的狀態,沒有特定的先後關係,所以也不難理解獲取的值是0了
再次回到主題,那為什麼View.post(),就可以獲取到準確的值呢,不妨猜測一下,首先整體上思考一下,想要實現知道準確的寬高,那就是post的Runnable那肯定是在View整個繪製流程結束之後才執行的,主執行緒又是基於Looper的訊息機制的,如果把Runnable直接作為一個訊息插入訊息佇列,那麼很明顯不能保證這種效果,熟悉View繪製流程的朋友知道,View的繪製是在ViewRootImp中的,但View的繪製其實也是一個Runnable訊息,那麼我們可不可以先把post的這個Runnable給快取起來,等到繪製的Runnable執行完之後,再來通知去執行,這樣就能夠獲取到準確的寬高了。
View##post
本文原始碼是API是android-28,不同版本可能有些差異,需要讀者自行注意
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
getRunQueue().post(action);
return true;
}
複製程式碼
可以看到方法很簡答,主要就是一個attachInfo,如果不為空就直接使用attachInfo.mHandler去執行這個action,如果為空,把Runnable放入一個類似佇列的東西里面
我們再回頭想想開頭說的話,好像還真是這麼實現的,這裡的mAttachInfo其實可以看做為是否已經繪製好了的一個標誌,如果不為空,說明繪製完成,直接handler執行action,如果為空,說明沒有繪製完,這時候就把Runnable快取起來,那麼關鍵點也就來了,這個mAttachInfo是什麼時候被賦值的,全域性搜尋下賦值
View##dispatchAttachedToWindow
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
......
}
複製程式碼
從這個方法名字,我們也應該能看出來,繫結到window的時候,此時會進行賦值mAttachInfo,也就意味著繪製完畢,當然,我們還不知道dispatchAttachedToWindow這個方法是什麼時候呼叫的,先這麼理解著
getRunQueue().post(action)
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
複製程式碼
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++;
}
}
複製程式碼
可以看到,實際上是維護了一個HandlerActionQueue類,內部維護了一個陣列,長度居然是固定為4(問號臉),然後將這些Runnabel給快取起來。那疑問就來了,既然是快取起來,那什麼時候執行的,可以看到有個executeActions方法
executeActions(Handler handler)
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;
}
}
複製程式碼
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
......
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
....
}
複製程式碼
通過傳遞進來的handler,然後將內部快取的Runnable去執行,ctrl一下,看看哪裡呼叫了,咦。又是dispatchAttachedToWindow這個方法。
先總結下,View.post(Runnable) 的這些 Runnable 操作,在 View 被 attachedToWindow 之前會先快取下來,然後在 dispatchAttachedToWindow() 被呼叫時,就將這些快取下來的 Runnable 通過 mAttachInfo 的 mHandler 來執行。在這之後再呼叫 View.post(Runnable) 的話,這些 Runnable 操作就不用再被快取了,而是直接交由 mAttachInfo 的 mHandler 來執行
所以問題最終也就到這個方法這裡了,這個方法是什麼時候被呼叫的,ctrl一下。。居然沒有地方呼叫,那肯定是隱藏類呼叫了,此時祭出我們的Source Insight,從原始碼裡面找找。
ViewRootImpl##performTraversals
private void performTraversals() {
.....
final View host = mView;
.....
host.dispatchAttachedToWindow(mAttachInfo, 0)
....
}
final ViewRootHandler mHandler = new ViewRootHandler();
public ViewRootImpl(Context context, Display display) {
...
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
....
}
複製程式碼
這個方法是不是很眼熟,View的繪製流程就從這個方法開始的,可以看到的是,mAttachInfo這個物件是在ViewRootImpl初始化的時候就賦值了,並且Handler是直接在主執行緒中建立的,這個就可以說明了為什麼View.post是可以更新UI的,因為最終的Runnable是在主執行緒的Handler中去執行的,自然是可以更新UI的。
但是大家有可能還有個疑問,那既然View的繪製也是這個方法執行的,dispatchAttachedToWindow也是在這個方法執行的,那怎麼能保證一定是在View的繪製流程完成之後才去執行dispatchAttachedToWindow的呢。
答案也很簡單,因為android主執行緒是基於Looper的訊息機制的,不斷的從繫結Looper的MessageQueue中去獲取message去執行,View的繪製操作其實也是一個Runnable物件,所以在執行performTraversals()方法的時候,呼叫dispatchAttachedToWindow方法,這個時候,所以子View通過View.post(Runnable)快取的Runnabel是會通過mAttachInfo.mHandler 的 post() 方法將這些 Runnable 封裝到 Message 裡傳送到 MessageQueue 裡。mHandler 我們上面也分析過了,繫結的是主執行緒的 Looper,所以這些 Runnable 其實都是傳送到主執行緒的 MessageQueue 裡排隊的,所以也就可以保證這些 Runnable 操作也就肯定會在 performMeasure() 操作之後才執行,寬高也就可以獲取到了,我們也可以在原始碼中找到些許痕跡
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
.....
performTraversals();
....
}
}
複製程式碼
總結
- View.post(Runnable)內部兩種判斷,如果當前View沒有繪製完成,通過類HandlerActionQueue內部將Runnabel快取下來,否則就直接通過 mAttachInfo.mHandler 將這些 Runnable 操作 post 到主執行緒的 MessageQueue 中等待執行。
- mAttachInfo.mHandler是ViewRootImpl中的成員變數,繫結的是主執行緒的Looper,所以View.post的操作會轉到主執行緒之中,自然可以作為更新UI的根據了
- Handler訊息機制是不斷的從佇列中獲取Message物件,所以 View.post(Runnable) 中的 Runnable 操作肯定會在 performMeaure() 之後才執行,所以此時可以獲取到 View 的寬高