在android中,如果在onCreate时直接使用view.getWidth()、view.getHeight()等方法获取view的宽高,会得的返回值0。通常我们可以借助view.post()来获取view的宽高,为什么这个方法就能拿到view的宽高值呢,今天对post()的流程做次梳理。
先看下post方法:
View.java
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是否为null,进行不同操作:
- attachInfo不为null时,调用其内部的mHandler实例发送一个Runnable类型的消息
- attachInfo为null时,使用HandlerActionQueue类型的实例发送Runnable消息
那么mAttachInfo的赋值就成了关键,通过查找我们发现其赋值有两处地方:
在dispatchAttachedToWindow()中赋值,在dispatchDetachedFromWindow()中设置为null。在dispatchAttachedToWindow()又是在哪里调用的呢?很可惜在源码中并没有查找到(其实是在ViewRootImpl$performTraversals()中调用了该方法),不过我们知道了attachInfo不为nul时发送了一个消息到消息队列。这条线暂时断了。
接下来看下attachInfo为null的情况:
HandlerActionQueue.java
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类型的实例其实是把消息放入了一个数组(mActions)中,后面并没有对消息进行操作,查看该类类结构:
发现有一个executeActions()方法,查阅该方法:
HandlerActionQueue.java
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;
}
}
复制代码
executeActions()方法很简单,就是遍历了数组mActions,把数组中的消息通过参数handler发送了出去。也就是说,无论mAttachInfo是否被实例化,最终都是通过handler把runnable发送出去。
查找一下executeActions()的调用地方,发现有两处调用了该方法:
- View$dispatchAttachedToWindow()方法中调用
- ViewRootImpl$performTraversals()方法中调用
在前面分析时我们我们未发现dispatchAttachedToWindow()在何处调用,线索已经中断,我们直接看ViewRootImpl$performTraversals()这一处调用。
我们知道android的绘制、布局、测量三大流程就是发生在performTraversals()中,通过简化后代码展示一下executeActions()和三大流程的先后关系:
ViewRootImpl.java
private void performTraversals() {
...
host.dispatchAttachedToWindow(mAttachInfo, 0);
...
getRunQueue().executeActions(mAttachInfo.mHandler);
...
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw()
}
复制代码
通过简化后代码可以看到,HandlerActionQueue.executeActions()方法是先于三大流程触发的,也就是说executeActions()中handle发送的消息是优先于三大流程的,这样就有一个问题,handle发消息之前并没有进行测量,如何可以正确的返回view的宽高?
这里简单介绍一下Message的类型:
- 同步屏障消息,msg.target为null,用于标识消息队列(messageQueue)中存在异步消息,非异步消息处理暂停
- 异步消息,消息队列中优先处理的消息,msg.isAsynchronous()返回值为true,屏幕刷新时会发送异步消息
- 普通消息,也就是平常我们自己发出的消息,msg.isAsynchronous()返回值为false
下面是MessageQueue获取消息的精简代码
MessageQueue.java
Message next() {
...
//从死循环获取下一消息
for (;;) {
...
nativePollOnce(ptr, nextPollTimeoutMillis);//阻塞或唤醒队列
...
synchronized (this) {
...
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
//Stalled by a barrier. Find the next asynchronous message in the queue.
//存在同步屏障消息,从队列获取异步消息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
...
//获取普通消息
...
}
}
复制代码
简单来说,当消息队列中存在异步消息时,队列的普通消息不会从队列取出来。直到异步消息处理完,同步屏障消息消失为止。
屏幕绘制就是一个异步消息,该消息执行完之后(即performTraversals方法执行完)才会执行消息队列中的普通消息。只不过view.post()消息触发时间是在异步消息执行过程中调用的。
当performTraversals()执行完,异步消息结束,消息队列取出其它普通消息,比如view.post(runnable),执行runnable,即获取view的宽高,因为在异步消息执行期间已经执行过测量,所以可以获取到view的宽高。
流程图如下:
同步屏障消息、异步消息学习:juejin.cn/post/695262…