筆者解惑原文,并對原文有所借鑒之處,特此聲明,讀者可一并閱讀原文:鏈接
慣例,導語:
最怕一生碌碌無為,還聊以自慰平淡是真。
在之前的文章《Android解決在onCreate中獲取View的width、Height為0的方法》提到過,可以通過View.post方式:
view.post(new Runnable() {
@Override
public void run() {
view.getHeight(); //height可用
}
});
之后有同學問到:
本著知其然知其所以然的學習態度,覺得還是有必要把為什么通過View.post方式就能獲取到View的width/height的原理捯飭捯飭。
首先,觀察View.post方法的實現:
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;
}
主要是根據attachInfo是否被初始化決定執行方式,那么attachInfo在Activity的onCreate()執行時到底是不是null呢?關于attachInfo的初始化,我們可以在View源碼中找到,其只有在dispatchAttachedToWindow()方法才被賦值,而dispatchAttachedToWindow()方法的調用是來自于ViewGroup,繼續向上層去找,我們就不得不追溯到ViewRootImpl的perFormTraversals()方法了,熟悉view流程的都知道,view的三大流程就是通過這個稱為“執行遍歷”的方法來完成的。但是這個方法有整整800行代碼,就只取主要流程的代碼了:
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView;
if (mFirst) {
···
host.dispatchAttachedToWindow(mAttachInfo, 0);
}
···
//先于performMeasure被執行了
getRunQueue().executeActions(attachInfo.mHandler);
...
performMeasure();
...
performLayout();
...
performDraw();
}
在這里,我們明確了attachInfo的初始化,在onCreate中執行View.post的時候,attachInfo還是null。回到post的代碼,確認執行的是 ViewRootImpl.getRunQueue().post(action) 的邏輯:
static final class RunQueue {
void post(Runnable action) {
postDelayed(action, 0);//沒有延時
}
void postDelayed(Runnable action, long delayMillis) {
HandlerAction handlerAction = new HandlerAction();
handlerAction.action = action;
handlerAction.delay = delayMillis;
synchronized (mActions) {
mActions.add(handlerAction);
}
}
}
RunQueue只是將需要執行的runnable消息暫時做一個存儲,并且此消息沒有延時。在前面ViewRootImpl.performTraversals()方法中我有注釋:
//先于performMeasure被執行了
getRunQueue().executeActions(attachInfo.mHandler);
...
performMeasure();
...
performLayout();
...
performDraw();
getRunQueue().executeActions()竟然先于performMeasure()執行了,這還了得嗎?如果是這樣的話,我們通過View.post()方式獲取的應該是還沒有測量過的寬高呀!
好吧,我們還要看一下RunQueue.executeActions()的實現:
void executeActions(Handler handler) {
synchronized (mActions) {
final ArrayList<HandlerAction> actions = mActions;
final int count = actions.size();
for (int i = 0; i < count; i++) {
final HandlerAction handlerAction = actions.get(i);
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
actions.clear();
}
}
這里面其實也是調用Handler去post我們的Runnable,而ViewRootImpl的Handler就是主線程的Handler,因此在performTraversals()被執行的Runnable其實是被主線程的Handler的post到執行隊列里面了。這里說明下,Android的運行其實是一個消息驅動模式,不了解消息機制的也可以看我的另一篇《Android源碼 從runOnUiThread聊聊消息機制》。
根據消息機制原理,我們需要等待主線程的Handler執行完當前的任務,才會去執行我們View.post的那個Runnable。
那么當前正在執行了什么任務呢?答案是TraversalRunnable,具體我們也要看ViewRootImpl的源碼,里面有TraversalRunnable的定義:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
···
performTraversals();
···
}
}
關于TraversalRunnable的調度時機,不再此篇范圍了。
到這里,我能回答開篇有同學提到的問題了吧:
View.post(runnable)方法的代碼會在view的draw方法之前調用么?
如果按照我們剛分析的performTraversals()方法的執行流程:
getRunQueue().executeActions(attachInfo.mHandler);
...
performMeasure();
...
performLayout();
...
performDraw();
那么答案是明確的:View.post(runnable)方法的代碼會在view的draw方法之前調用。
但,這是真的嗎?不是!
OMG! 為毛?我曾也天真的以為。
我還是去做了實驗,結果:
注意到了沒?measure被執行了三次,layout被執行了兩次,中間穿插了post的Runnable的執行結果,然后在第二次的layout之后才會去執行draw流程!
通過上面的分析,可以明確的是:第一次layout和第二次layout應該是兩個不同的任務。因為在這中間已經有了View.post的Runnable的執行結果,所以有了結論是:一共有三個任務,第一次performTraversals、我們的Runnable、第二次performTraversals。
那么為什么會執行兩次performTraversals呢?還是要回到performTraversal()方法中,取出與performDraw相關的代碼:
......
if (!cancelDraw && !newSurface) {
if (!skipDraw || mReportNextDraw) {
......
performDraw();
}
} else {
if (viewVisibility == View.VISIBLE) {
scheduleTraversals();
} else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).endChangingAnimations();
}
mPendingTransitions.clear();
}
}
......
可以看出,當newSurface為真時,performTraversals函數并不會調用performDraw函數,而是調用scheduleTraversals函數,從而再次調用一次performTraversals函數,從而再次進行一次測量,布局和繪制過程。
到這里終于有了明確答案了:
View.post(runnable)方法的代碼不會在view的draw方法之前調用。
但是Android系統設計時,為什么要將整個初始化過程設計成這樣?為什么當Surface為新的時候,要推遲繪制,重新進行一輪初始化?
希望有經驗的同學解惑啊,歡迎討論。