为什么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消息机制之同步屏障

相关推荐
EricWang13586 分钟前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning6 分钟前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人16 分钟前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
超雄代码狂38 分钟前
ajax关于axios库的运用小案例
前端·javascript·ajax
长弓三石1 小时前
鸿蒙网络编程系列44-仓颉版HttpRequest上传文件示例
前端·网络·华为·harmonyos·鸿蒙
小马哥编程1 小时前
【前端基础】CSS基础
前端·css
嚣张农民1 小时前
推荐3个实用的760°全景框架
前端·vue.js·程序员
周亚鑫1 小时前
vue3 pdf base64转成文件流打开
前端·javascript·pdf
Justinc.2 小时前
CSS3新增边框属性(五)
前端·css·css3
neter.asia2 小时前
vue中如何关闭eslint检测?
前端·javascript·vue.js