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

145 阅读4分钟

我们经常会遇到要获取 view 的宽高的情况,如果直接在 onCreate() 方法中获取 View 的宽高,拿到的结果是 0,但是通过 view 的 post() 方法却可以拿到 view 的宽高,运行如下代码:

public class MyActivity extends AppCompatActivity {

    private static final String TAG = MyActivity.class.getSimpleName();

    private TextView tv;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tv = (TextView) findViewById(R.id.my_text);
        Log.d(TAG, "11111 width: " + tv.getMeasuredWidth() + " - height : " + tv.getHeight());
        tv.post(new Runnable() {

            @Override
            public void run() {
                Log.d(TAG, "22222 width: " + tv.getMeasuredWidth() + " - height : " + tv.getMeasuredHeight());
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.e(TAG, "33333 height:" + tv.getMeasuredHeight());
    }
}

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/my_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />
</LinearLayout>

打印结果如下:

11111 width: 0 - height : 0
33333 height:0
22222 width: 201 - height : 51

其中 getMeasuredWidth() 获取的是成员变量 mMeasuredWidth 的值:

public final int getMeasuredWidth() {
    return mMeasuredWidth & MEASURED_SIZE_MASK;
}

通过前面的文章 Android View的绘制流程 我们知道执行完 View 的 measure() 方法才会对 mMeasuredWidth 赋值,第一次触发绘制在 OnResume() 生命周期方法调用之后,为什么这里在 OnCreate() 方法里面执行 tv.post(Runnable action) 却可以获取到View的宽高呢?

下面我们就围绕这个疑问通过源码来分析,源码基于Android SDK 31。

先来看看 View 的 post() 方法做了什么:

public class View{

    public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        // 若 attachInfo 不为 null,直接调用其内部 Handler 的 post() 方法
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action);
        }

        // 下面是 attachInfo 为 null 的情况
        getRunQueue().post(action);
        return true;
   }
}

这里通过判断 attachInfo 是否为 null 进行了不同的处理,我们先来研究 attachInfo 为 null 的情况, attachInfo 为 null 时调用了 getRunQueue() 的 post() 方法:

public class View{

    /**
    * Queue of pending runnables. Used to postpone calls to post() until this
    * view is attached and has a handler.
    */
    private HandlerActionQueue mRunQueue;

    private HandlerActionQueue getRunQueue() {
        if (mRunQueue == null) {
            mRunQueue = new HandlerActionQueue();
        }
        return mRunQueue;
    }
}

其中 getRunQueue() 返回的是 mRunQueue,它是是 HandlerActionQueue 的实例,HandlerActionQueue 的 post() 方法的代码如下:

public class HandlerActionQueue {

    private HandlerAction[] mActions;

    public void post(Runnable action) {
        postDelayed(action, 0);
    }

    public void postDelayed(Runnable action, long delayMillis) {
        // 1. 将传入的 Runnable 封装成 HandlerAction
        final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

        synchronized (this) {
            if (mActions == null) {
                mActions = new HandlerAction[4];
            }
            // 2. 将 HandlerAction 保存在 mActions 数组中
            mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
            mCount++;
        }
    }
}

这里先将传入的 action 封装成 HandlerAction 对象,然后将 HandlerAction 对象保存到了 mActions 数组中。由此可知 attachInfo 为 null 时,post(Runnable action) 只是把 action 添加到 mActions 数组里面存了起来,暂时还没有执行。而执行 View 的 post() 方法时 attachInfo 是否为 null 呢?答案是肯定的,View 中只有一处给 mAttachInfo 赋值的地方,在 dispatchAttachedToWindow() 方法中:

public class View{

    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        // 给 mAttachInfo 赋值,此时同一个 ViewRootImpl 内的所有 View 共用同一个 AttachInfo
        mAttachInfo = info;
        ...
        // mRunQueue 又出现了,其内部保存了我们的 action 任务
        if (mRunQueue != null) {
            // 内部执行了 info.mHandler.post(action)
            mRunQueue.executeActions(info.mHandler);
            mRunQueue = null;
        }
        ...
    }
}

在这里给 mAttachInfo 赋值后,使用 Handler 执行 mActions 中存储的任务,所以这里才会真正执行 Handler.post(action)

View 的 dispatchAttachedToWindow() 又是在什么时候调用的呢?在绘制流程的开始阶段会调用 ViewRootImpl 的 performTraversals() 方法:

public final class ViewRootImpl{
    /**
    * 1. AttachInfo 的创建是在 ViewRootImpl 的构造方法中
    * 2. 同一个 View Hierachy 树结构中所有的 View 共用一个 AttachInfo
    */
    public ViewRootImpl(...){
        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,context);
    }

    private void performTraversals() {
        // mView 即 DecorView,host 的类型是 DecorView,DecorView 继承自 FrameLayout
        // 每个 Activity 都会关联一个 Window,每个 Window 又对应一个 DecorView 对象
        final View host = mView;

        // 调用 DecorView 的 dispatchAttachedToWindow()
        // 关注 1
        host.dispatchAttachedToWindow(mAttachInfo, 0);

        // 开始绘制的三大流程:测量、布局、绘制
        performMeasure();
        performLayout();
        performDraw();
        ...
    }
}

这里会调用 DecorView 的 dispatchAttachedToWindow() 方法,DecorView 中并没有这个方法,该方法在 DecorView 的父类——ViewGroup 中:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        super.dispatchAttachedToWindow(info, visibility);

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

        // 遍历所有子 View,调用所有子 View 的 dispatchAttachedToWindow() & 为每个子 View 关联 AttachInfo
        for (int i = 0; i < count; i++) {
        final View child = children[i];
            child.dispatchAttachedToWindow(info,combineVisibility(visibility, child.getVisibility()));
        }
    }
}

在这里会遍历 DecorView 中的子 View 并执行子 View 的 dispatchAttachedToWindow() 方法,所以,View 的 dispatchAttachedToWindow() 方法是在 ViewRootImpl 的 performTraversals() 方法中调用的

而 ViewRootImpl 的 performTraversals() 方法的执行在 onResume() 之后,在文章最开始的示例中,tv.post(Runnable action) 是在 onCreate() 方法中调用的,所以此时 mAttachInfo 为 null。

真正执行 Handler.post(action) 在关注 1 处,但是上面的关注 1 明明是在绘制流程开始之前执行的,这样执行 action 获取出来的宽高不应该是 0 吗?

这是因为在绘制流程开始之前代码里给 mHandler 添加了同步屏障消息,此时会优先处理异步消息,即优先处理绘制流程

public final class ViewRootImpl{

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            // 添加同步屏障
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 开始绘制流程
            mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        }
    }
}

只有等绘制流程结束后,才会处理 mHandler 中的同步消息,这时候才会执行 action 的 run() 方法,所以 action 的 run() 方法其实是在 measure() 方法之后执行的,所以在这里可以获取到正确的宽高值。

至于什么是同步屏障,大家看这里:Android消息机制之同步屏障