今天遇到一个很莫名其妙的问题,就是一个view有两个状态,状态A与状态B,在Activity的onCreate的时候先显示状态A,并在onCreate里面请求接口,接口回来后显示B。
断点的时候,这个逻辑没问题,但是放开断点,很大几率显示的是view的状态A,原因就在于View.post()执行的时机不定,今天这篇文章就来追溯一下View.post()执行的时机。
相信大家都知道,我们如果想在Activity.onCreate()里面计算宽高,就可以使用View.post()的方法,并且,在没有Handler的时候,我们可以拿某个View代替Handler的功能,使用View.post(),但是View.post()并不一定会执行,下面我们来看下源码。
/**
* Causes the Runnable to be added to the message queue.
* The runnable will be run on the user interface thread.
*
* @param action The Runnable that will be executed.
*
* @return Returns true if the Runnable was successfully placed in to the
* message queue. Returns false on failure, usually because the
* looper processing the message queue is exiting.
*
* @see #postDelayed
* @see #removeCallbacks
*/
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
相信大家跟我一样,很容易忽略,原来post是有返回值的,不过这个返回值其实没有意义,注释也说了,一般是队列退出的时候才这样。
从这里可以看到,这里的attachInfo很关键,这个attchInfo是什么东西呢?
其实,在之前的博客隐约提过这个,attachInfo是在ViewRootImpl里面赋值的,ViewRootImpl是个很复杂的类,几乎跟view有关的所有核心代码都在这里面,他也是连接WMS和View的桥梁,是所有View的根View,注意,ViewRootImpl并不是个View,只是View。网上关于ViewRoot的解析文章汗牛充栋,楼主也是看他们的文章,这里就不打算自己写了,只聊聊我们这里的问题。
View中AttchInfo的赋值是在performTraversals里面
//ViewRootImpl performTraversals()
Rect frame = mWinFrame;
if (mFirst) {
.....省略代码
host.dispatchAttachedToWindow(mAttachInfo, 0);
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
dispatchApplyInsets(host);
//Log.i(mTag, "Screen on initialized: " + attachInfo.mKeepScreenOn);
} else {
...
}
//View dispatchOnWindowAttachedChange()
public void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
...省略
if (mRunQueue != null) {
//这里就是消耗的地方
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
}
但是,ViewRotImpl里面的AttachInfo的却是在初始化里面
public ViewRootImpl(Context context, Display display) {
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,context);
}
由此可见,mAttachInfo的赋值,是在perfomTravels里面,而perfomTravels又是在响应onVsync信号的时候才赋值,而监听垂直信号量又是在Activity的生命周期performRusume()里面的wm.addView()里面执行的。由此可见,在onCreate里面执行的post,并不会执行,而是会走getRunQueue().post(action);
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
/**
* Returns the queue of runnable for this view.
*
* @return the queue of runnables for this view
*/
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
HandlerActionQueue是什么呢?官方对于这个类的解释已经很清楚了
Class used to enqueue pending work from Views when no Handler is attached.
也就是说,是在View没有被attach的时候,临时存储工作的。
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];
}
//这个地方其实有一个小小的疑惑,那就是GrowingArrayUtils是V7包里面的类,android原生包并没有,这里是怎么调用成功的。又或者GrowingArrayUtils其实存在只是被隐藏了?
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}
HanderAction就是用来存储Runnable和delay时间的。
而HandlerAction的消耗就是在executeActions,上面已经在代码里面写过,就是在View.dispatchAttachedToWindow()里面
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;
}
}
注意,这里是只是把这个action发送到ViewRootImple的子线程去了!好了,这个问题基本在这里就解决了,那么,上面几个问题答案也就出来了。
弄清楚这个问题,我们就需要搞清楚,在dispatchAttachedToWindow之前,view宽高是否已经测量好了。这个问题其实也很简单,留给读者自己去阅读源码。其实在上面已经解释了,执行executeActions其实只是把Runnable发送到ViewRootImple里面去,很明显,再次执行这个action的时候,performTravels已经执行完成了。
这就是因为view不一定会AttachTo到ViewRootImpl上面,如果没有添加到这个ViewRoot里面,显然就不会执行了。并且,前面也分析过,这个执行时间是无法确定的,所以经常出现断点时可以,但是放开断点就有问题的情况。
但是,注意,这里以API 24为分界线,API24之前,并不是加入ViewRoot里面才执行,具体可以参考博客直面底层:你真的了解 View.post() 原理吗?
这个知识点很危险,如果不知道,就经常容易用错,由此可见,平时多看源码,多看博客是多么重要。