笔者解惑原文,并对原文有所借鉴之处,特此声明,读者可一并阅读原文:链接
惯例,导语:
最怕一生碌碌无为,还聊以自慰平淡是真。
在之前的文章《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为新的时候,要推迟绘制,重新进行一轮初始化?
希望有经验的同学解惑啊,欢迎讨论。