View$post()获取View宽高流程梳理

在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的赋值就成了关键,通过查找我们发现其赋值有两处地方:

1.png
在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)中,后面并没有对消息进行操作,查看该类类结构:

2.png

发现有一个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()的调用地方,发现有两处调用了该方法:

3.png

  • 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的类型:

  1. 同步屏障消息,msg.target为null,用于标识消息队列(messageQueue)中存在异步消息,非异步消息处理暂停
  2. 异步消息,消息队列中优先处理的消息,msg.isAsynchronous()返回值为true,屏幕刷新时会发送异步消息
  3. 普通消息,也就是平常我们自己发出的消息,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的宽高。
流程图如下:

未命名文件.png
同步屏障消息、异步消息学习:juejin.cn/post/695262…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享