筆者解惑原文杰捂,并對原文有所借鑒之處舆床,特此聲明,讀者可一并閱讀原文:鏈接
慣例嫁佳,導語:
最怕一生碌碌無為挨队,還聊以自慰平淡是真。
在之前的文章《Android解決在onCreate中獲取View的width蒿往、Height為0的方法》提到過盛垦,可以通過View.post方式:
view.post(new Runnable() {
@Override
public void run() {
view.getHeight(); //height可用
}
});
之后有同學問到:
本著知其然知其所以然的學習態(tài)度,覺得還是有必要把為什么通過View.post方式就能獲取到View的width/height的原理捯飭捯飭熄浓。
首先情臭,觀察View.post方法的實現(xiàn):
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;
}
主要是根據(jù)attachInfo是否被初始化決定執(zhí)行方式,那么attachInfo在Activity的onCreate()執(zhí)行時到底是不是null呢赌蔑?關于attachInfo的初始化俯在,我們可以在View源碼中找到,其只有在dispatchAttachedToWindow()方法才被賦值娃惯,而dispatchAttachedToWindow()方法的調用是來自于ViewGroup跷乐,繼續(xù)向上層去找,我們就不得不追溯到ViewRootImpl的perFormTraversals()方法了趾浅,熟悉view流程的都知道愕提,view的三大流程就是通過這個稱為“執(zhí)行遍歷”的方法來完成的馒稍。但是這個方法有整整800行代碼,就只取主要流程的代碼了:
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView;
if (mFirst) {
···
host.dispatchAttachedToWindow(mAttachInfo, 0);
}
···
//先于performMeasure被執(zhí)行了
getRunQueue().executeActions(attachInfo.mHandler);
...
performMeasure();
...
performLayout();
...
performDraw();
}
在這里浅侨,我們明確了attachInfo的初始化纽谒,在onCreate中執(zhí)行View.post的時候,attachInfo還是null如输」那回到post的代碼,確認執(zhí)行的是 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只是將需要執(zhí)行的runnable消息暫時做一個存儲不见,并且此消息沒有延時澳化。在前面ViewRootImpl.performTraversals()方法中我有注釋:
//先于performMeasure被執(zhí)行了
getRunQueue().executeActions(attachInfo.mHandler);
...
performMeasure();
...
performLayout();
...
performDraw();
getRunQueue().executeActions()竟然先于performMeasure()執(zhí)行了,這還了得嗎稳吮?如果是這樣的話缎谷,我們通過View.post()方式獲取的應該是還沒有測量過的寬高呀!
好吧灶似,我們還要看一下RunQueue.executeActions()的實現(xiàn):
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()被執(zhí)行的Runnable其實是被主線程的Handler的post到執(zhí)行隊列里面了喻奥。這里說明下席纽,Android的運行其實是一個消息驅動模式,不了解消息機制的也可以看我的另一篇《Android源碼 從runOnUiThread聊聊消息機制》撞蚕。
根據(jù)消息機制原理,我們需要等待主線程的Handler執(zhí)行完當前的任務过牙,才會去執(zhí)行我們View.post的那個Runnable甥厦。
那么當前正在執(zhí)行了什么任務呢?答案是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()方法的執(zhí)行流程:
getRunQueue().executeActions(attachInfo.mHandler);
...
performMeasure();
...
performLayout();
...
performDraw();
那么答案是明確的:View.post(runnable)方法的代碼會在view的draw方法之前調用。
但撵溃,這是真的嗎疚鲤?不是!
OMG! 為毛缘挑?我曾也天真的以為集歇。
我還是去做了實驗,結果:
注意到了沒语淘?measure被執(zhí)行了三次诲宇,layout被執(zhí)行了兩次际歼,中間穿插了post的Runnable的執(zhí)行結果,然后在第二次的layout之后才會去執(zhí)行draw流程姑蓝!
通過上面的分析鹅心,可以明確的是:第一次layout和第二次layout應該是兩個不同的任務。因為在這中間已經有了View.post的Runnable的執(zhí)行結果纺荧,所以有了結論是:一共有三個任務旭愧,第一次performTraversals、我們的Runnable虐秋、第二次performTraversals榕茧。
那么為什么會執(zhí)行兩次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函數(shù)并不會調用performDraw函數(shù)靶剑,而是調用scheduleTraversals函數(shù)蜻拨,從而再次調用一次performTraversals函數(shù),從而再次進行一次測量桩引,布局和繪制過程缎讼。
到這里終于有了明確答案了:
View.post(runnable)方法的代碼不會在view的draw方法之前調用。
但是Android系統(tǒng)設計時坑匠,為什么要將整個初始化過程設計成這樣血崭?為什么當Surface為新的時候,要推遲繪制厘灼,重新進行一輪初始化?
希望有經驗的同學解惑啊夹纫,歡迎討論。