为什么view.post()能保证获取到view的宽高?

转载自 Carson带你学Android:为什么view.post()能保证获取到view的宽高?

  • 背景
    业务需求代码开始时机一般是在:Activity的生命周期onCreate()
    视图View 绘制时机:Activity的生命周期onResume()之后

注:ActivityThread 的 handleResumeActivity()执行顺序:先回调 Activity 生命周期 onResume() - 再开始 View 的绘制任务


  • 矛盾
    业务需求代码需获取宽高的时机 跟 View的绘制时机 存在时序问题
    一般来说,业务需求代码开始时就需要获取View的相关信息(如宽、高),但:View 绘制时机在Activity.onResume()之后,即在Activity.onCreate()之后 = 业务需求代码开始后。

  • 解决方案
    将需执行的任务传入到View.post() 。这个操作大家一定很熟悉。那么 其内部原理是什么呢?

  • 结论
    以Handler为基础,View.post() 将传入任务的执行时机调整到View 绘制完成之后。

源码解析

我们直接从view.post()入手:

public boolean post(Runnable action) {
    
    // 仅贴出关键代码
    // ...
    
    // 判断AttachInfo是否为null
    final AttachInfo attachInfo = mAttachInfo;

    // 过程1:若不为null,直接调用其内部Handler的post
    if (attachInfo != null) {
        
        return attachInfo.mHandler.post(action);
    }

    // 过程2:若为null,则加入当前View的等待队列
    getRunQueue().post(action); 
    // getRunQueue() 返回的是 HandlerActionQueue
    // action代表传入的要执行的任务
    // 即调用了HandlerActionQueue.post() ->> 分析1
    return true;
}

此处分成两个过程讲解:

当AttachInfo不为null时,直接调用其内部Handler的post;
当AttachInfo为null时,则将任务加入当前View的等待队列中。
此处为了方便理解,我会先讲解过程2

过程2:当AttachInfo为null时,则将任务加入当前View的等待队列中。

public boolean post(Runnable action) {
    
    // 仅贴出关键代码
    // ...
    
    // 判断AttachInfo是否为null
    final AttachInfo attachInfo = mAttachInfo;

    // 过程1:若不为null,直接调用其内部Handler的post
    if (attachInfo != null) {
        
        return attachInfo.mHandler.post(action);
    }

    // 过程2:若为null,则加入当前View的等待队列
    getRunQueue().post(action); 
    // getRunQueue() 返回的是 HandlerActionQueue
    // action代表传入的要执行的任务
    // 即调用了HandlerActionQueue.post() ->> 分析1
    return true;
}

/**
  * 分析1:HandlerActionQueue.post()
  */
public void post(Runnable action) {
    // ...
    postDelayed(action, 0); 
    // ->> 分析2 
}

/**
  * 分析2
  */
public void postDelayed(Runnable action, long delayMillis) {
    // 1. 将传入的任务runnable封装成HandlerAction  ->>分析3
    final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

    synchronized (this) {
        // 2. 将要执行的HandlerAction 保存在 mActions 数组中
        if (mActions == null) {
            mActions = new HandlerAction[4];
        }

        mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
        mCount++;
    }
}

/**
  * 分析3:HandlerAction 表示一个待执行的任务
  * 内部持有要执行的 Runnable 和延迟时间
  */
private static class HandlerAction {
    // post的任务
    final Runnable action;
    // 延迟时间
    final long delay;

    public HandlerAction(Runnable action, long delay) {
        this.action = action;
        this.delay = delay;
    }
   // ...
}

结论:
将传入的任务封装成HandlerAction对象
创建一个默认长度为4的 HandlerAction数组,用于保存通过post()添加的任务

注:此时只是保存了通过post()添加的任务,并没执行。

过程1:当AttachInfo不为null时,直接调用其内部Handler的post()

public boolean post(Runnable action) {
    
    // ...
    
    // 判断AttachInfo是否为null
    final AttachInfo attachInfo = mAttachInfo;

    // 过程1:若不为null,直接调用其内部Handler的post ->>分析1
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }

    // 过程2:若为null,则加入当前View的等待队列
    getRunQueue().post(action); 
    return true;
}

/**
  * 分析1:AttachInfo的赋值过程 -> dispatchAttachedToWindow()
  * 注:AttachInfo持有当前渲染线程Handler
  */
  void dispatchAttachedToWindow(AttachInfo info, int visibility) {

    // ...

    // 给当前View赋值AttachInfo,此时同一个ViewRootImpl内的所有View共用同一个AttachInfo
    mAttachInfo = info;

    //  mRunQueue,即 前面说的 HandlerActionQueue
    // 其内部保存了当前View.post的任务
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        // 执行使用View.post的任务,post到渲染线程的Handler中
        // ->> 分析2
    }

    // 在Activity的onResume()中调用,但是在View绘制流程之前
    onAttachedToWindow();
    
    ListenerInfo li = mListenerInfo;
    final CopyOnWriteArrayList listeners =
            li != null ? li.mOnAttachStateChangeListeners : null;
    if (listeners != null && listeners.size() > 0) {
        for (OnAttachStateChangeListener listener : listeners) {
            // 通知所有监听View已经onAttachToWindow的客户端,即view.addOnAttachStateChangeListener();
            // 但此时View还没有开始绘制,不能正确获取测量大小或View实际大小
            listener.onViewAttachedToWindow(this);
        }
    }

    // ... 
}

/**
  * 分析2:HandlerActionQueue.executeActions()
  */
  public void executeActions(Handler handler) {
    synchronized (this) {
        
        final HandlerAction[] actions = mActions;
        // 任务队列
        // mActions即为前面过程1 保存了通过post()添加的任务 的数组

        // 遍历所有任务
        for (int i = 0, count = mCount; i < count; i++) {
            final HandlerAction handlerAction = actions[i];
            // 发送到Handler中,等待执行
            handler.postDelayed(handlerAction.action, handlerAction.delay);
        }

        // 此时不再需要后续的post,将被添加到AttachInfo中
        mActions = null;
        mCount = 0;
    }
}
// ->> 回到分析原处

结论
在AttachInfo被赋值时(即不为null),就会遍历 前面过程1保存了通过post()添加的任务 的数组,将每个任务发送到handler中等待执行。

下面,我们继续看,AttachInfo的赋值过程 -> dispatchAttachedToWindow()是什么时候被调用的。

  答:dispatchAttachedToWindow()调用时机是在 View 绘制流程
      的开始阶段,即 ViewRootImpl.performTraversals()
/**
  * 基础:
  * 1. AttachInfo的创建是在ViewRootImpl的构造方法中
  * 2. 同一个 View Hierachy 树结构中所有View共用一个AttachInfo
  */
  mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,context);

/**
  * dispatchAttachedToWindow()调用时机 = View绘制流程开始
  * 即ViewRootImpl.performTraversals()
  */
    private void performTraversals() {

        // ...

        // mView是DecorView
        // host的类型是 DecorView(继承自 FrameLayout)
        // 每个Activity都有一个关联的 Window(当前窗口),每个窗口内部又包含一个 DecorView 对象(描述窗口的xml视图布局)
        final View host = mView;

        // 调用DecorVIew的dispatchAttachedToWindow()
        // ->> 分析1
        host.dispatchAttachedToWindow(mAttachInfo, 0);
       
       getRunQueue().executeActions(mAttachInfo.mHandler);

       // 开始View绘制流程的测量、布局、绘制阶段
       performMeasure();
       performLayout();
       performDraw();
       ...

    }

/**
  * 分析1:DecorVIew.dispatchAttachedToWindow()
  * 注:DecorView并无重写该方法,而是在其父类ViewGroup里 
  */
  void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    super.dispatchAttachedToWindow(info, visibility);

    // 子View的数量
    final int count = mChildrenCount;
    final View[] children = mChildren;

    // 遍历所有子View,调用所有子View的dispatchAttachedToWindow() & 为每个子View关联AttachInfo
    // 子View 的 dispatchAttachedToWindow()在前面过程1已经分析过:
    // 即 遍历 前面过程1保存了通过post()添加的任务 的数组,将每个任务发送到handler中等待执行。
    for (int i = 0; i < count; i++) {
        final View child = children[i];
        child.dispatchAttachedToWindow(info,combineVisibility(visibility, child.getVisibility()));
    }
   
    // ...
}

结论
通过View.post()添加的任务是在View绘制任务里 - 开始绘制阶段时添加到消息队列的尾部的;
所以,View.post() 添加的任务的执行是在View绘制任务后才执行,即在View绘制流程结束之后执行
即View.post() 添加的任务能够保证在所有 View绘制流程结束之后才被执行,所以 执行View.post() 添加的任务时可以正确获取到 View 的宽高。

额外延伸

a. 问题描述
若只是创建一个 View & 调用它的post(),那么post的任务会不会被执行?

final View view = new View(this);

    view.post(new Runnable() {
        @Override
        public void run() {
            // ...
        }
    });

b. 答案
不会。主要原因是:

每个View中post() 需执行的任务,必须得添加到窗口视图-执行绘制流程 - 任务才会被post到消息队列里去等待执行,即依赖于dispatchAttachedToWindow ();

若View未添加到窗口视图,那么就不会走绘制流程,post() 添加的任务最终不会被post到消息队列里,即得不到执行。(但会保存到HandlerAction数组里)

上述例子,因为它没有被添加到窗口视图,所以不会走绘制流程,所以该任务最终不会被post到消息队列里 & 执行

c. 解决方案
此时只需要添加将View添加到窗口,那么post()的任务即可被执行

// 因为此时会重新发起绘制流程,post的任务会被放到消息队列里,所以会被执行
contentView.addView(view);
至此,关于view.post()原理讲解完毕

总结

    View.post()的原理:以Handler为基础,
    View.post() 将传入任务添加到 View绘制任务所在的消息队列
    尾部,从而保证View.post() 任务的执行时机是在View
    绘制任务完成之后的。 其中,几个关键点:

1-View.post()实际操作:将view.post()传入的任务保存到一个数组里
2-View.post()添加的任务 添加到 View绘制任务所在的消息队列尾部的时机:View 绘制流程的开始阶段,即 ViewRootImpl.performTraversals()
3-View.post()添加的任务执行时机:在View绘制任务之后

你可能感兴趣的:(为什么view.post()能保证获取到view的宽高?)