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

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

java 复制代码
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打印的是TextView测量后的宽高
                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 复制代码
<?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>

打印如下:

java 复制代码
 11111 width: 0 - height : 0
 33333 height:0
 22222 width: 201 - height : 51

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

java 复制代码
public final int getMeasuredWidth() {
    return mMeasuredWidth & MEASURED_SIZE_MASK;
}

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

下面我们就围绕这个疑问通过源码来进行研究,源码基于Android SDK 31。

java 复制代码
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的情况:

java 复制代码
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;
    }
}

mRunQueue是HandlerActionQueue的实例:

java 复制代码
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++;
       }
    }
}

由此可以看到attachInfo为null时,post(Runnable action)方法只把action添加到mActions数组里面了,暂时还没有执行。而此时attachInfo是否为null呢?答案是肯定的,View中只有一处给mAttachInfo赋值的地方,在dispatchAttachedToWindow()方法里面:

java 复制代码
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    // 给当前View赋值AttachInfo,此时同一个ViewRootImpl内的所有View共用同一个AttachInfo
    mAttachInfo = info;

    // mRunQueue又出现了,其内部保存了我们的action任务
    if (mRunQueue != null) {
    	//内部执行了info.mHandler.post(action)
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
}

在这里给mAttachInfo赋值后,将每个任务发送到handler中等待执行。

dispatchAttachedToWindow()是什么时候调用的呢?dispatchAttachedToWindow()调用时机是在绘制流程的开始阶段,在ViewRootImpl.performTraversals()里面:

java 复制代码
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(继承自 FrameLayout)
        //每个Activity都有一个关联的 Window(当前窗口),每个窗口内部又包含一个 DecorView对象(描述窗口的xml视图布局)
        final View host = mView;

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

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

    }
}


public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    /**
      * 关注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()在前面已经分析过了
        for (int i = 0; i < count; i++) {
            final View child = children[i];
            child.dispatchAttachedToWindow(info,combineVisibility(visibility, child.getVisibility()));
        }

    }

}

ViewRootImpl.performTraversals()是在Activity的生命周期方法onResume()执行之后才开始执行的,在最开始的示例中,tv.post()是在onCreate()方法调用的中,所以此时mAttachInfo为null。

mAttachInfo赋值的时机确认清楚了,但是上面的关注1中的host.dispatchAttachedToWindow(mAttachInfo, 0)明明在绘制流程开始之前执行的,这样获取出来的宽高不应该是0吗?

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

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

只有等绘制流程结束后,才会处理mHandler中的同步消息,这时候才会执行tv.getMeasuredWidth(),这样就获取到了正确的宽高值。

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

相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax