Android 开发探秘:View.post()为何能获取View宽高

21 阅读7分钟

Android 开发探秘:View.post()为何能获取View宽高

开发中的小困惑

在 Android 开发的奇妙世界里,获取 View 的宽高是一个常见的操作,但也常常让开发者们感到困惑。你是否曾经遇到过这样的情况:在 Activity 的onCreate方法中,信心满满地调用view.getWidth()view.getHeight(),满心期待能得到正确的宽高值,结果却得到了 0?这是不是让你感到十分疑惑,甚至有点抓狂呢🤯

例如,我们在布局文件中定义了一个简单的 TextView:


<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>

然后在 Activity 的onCreate方法中尝试获取它的宽高:


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 = findViewById(R.id.my_text);
        Log.d(TAG, "11111 width: " + tv.getMeasuredWidth() + " - height : " + tv.getHeight());
    }
}

运行后你会发现,日志输出的宽高都是 0。这是因为在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 = 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());
            }
        });
    }
}

这次运行后,日志中22222对应的宽高就不再是 0 了,而是正确的数值。这到底是为什么呢🧐 为什么view.post()有这样神奇的魔力?接下来,就让我们一起深入探究其中的奥秘。

初窥 View.post ()

()

在 Android 的世界里,View.post()方法就像是一个神秘的小助手。它的作用是将一个Runnable任务投递到主线程的消息队列中去执行 。简单来说,当你调用view.post(Runnable action)时,你传入的Runnable代码块并不会立即执行,而是会被添加到主线程消息队列的末尾,等待主线程在合适的时候(也就是消息队列中前面的任务都执行完后)来执行它。

而它在获取 View 宽高这件事上,有着独特的作用。就像我们前面提到的例子,在onCreate中直接获取宽高失败,但使用view.post()就能成功获取。这就像是它掌握了获取宽高的 “正确时机” 的秘诀,那么这个秘诀到底是什么呢🧐 接下来我们就深入到源码的世界去一探究竟。

常规获取宽高的 “尴尬”

在 Android 开发中,我们常常在 Activity 的onCreateonStartonResume等生命周期方法中尝试获取 View 的宽高 。但往往会得到令人失望的 0 值。这是为什么呢🧐

这要从 View 的绘制流程说起。View 的绘制流程主要分为三个阶段:测量(measure)、布局(layout)和绘制(draw)。在测量阶段,系统会计算 View 的宽高,确定measuredWidthmeasuredHeight;布局阶段则确定 View 在父容器中的位置;最后绘制阶段将 View 绘制到屏幕上 。

而在onCreateonStart等方法执行时,View 还处于初始化阶段,远远没有完成测量和布局过程。就好比你正在建造一座房子,房子还只是画了设计图,连地基都还没打,你就问房子有多高多大,这显然是不合理的,因为它还没有成型呢。所以此时调用view.getWidth()view.getHeight()方法获取宽高,得到的自然是 0 。只有当 View 完成了测量和布局,这些方法才能获取到正确的宽高值 。这也就解释了为什么在常规的生命周期方法中直接获取宽高会失败。

View.post () 神奇揭秘

1. 源码解读

要解开view.post()能获取 View 宽高的谜题,我们得深入到它的源码中去。View.post()方法的源码如下:


public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
    getRunQueue().post(action);
    return true;
}

从这段源码中,我们可以看到它首先判断AttachInfo是否为空 。AttachInfo是 View 的一个内部类,它保存了 View 与窗口相关的一些信息,每个 View 都会持有一个AttachInfo,默认情况下它是null

如果attachInfo不为空,就直接调用attachInfo.mHandler.post(action)。这里的mHandler是主线程的Handler,这意味着可以将任务直接通过主线程的Handler发送到主线程的消息队列中去执行 。

而当attachInfo为空时,就会调用getRunQueue().post(action)getRunQueue()方法返回的是一个HandlerActionQueue对象:


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

HandlerActionQueue类的post方法如下:


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++;
    }
}

可以看到,它会将传入的Runnable封装成一个HandlerAction对象,并将其添加到一个数组mActions中进行缓存 。简单来说,当attachInfo为空时,View.post()会把任务先缓存起来,等待后续执行。

2. 关键流程追踪

我们知道了attachInfo为空时任务会被缓存,那么这些缓存的任务什么时候会被执行呢🧐 这就涉及到attachInfo的赋值过程了。在 View 的源码中,只有一处给mAttachInfo赋值的地方,是在dispatchAttachedToWindow方法中:


void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
    // 其他代码...
}

View执行dispatchAttachedToWindow方法时,会给mAttachInfo赋值,并且会执行之前缓存的任务 。而dispatchAttachedToWindow方法是在 View 绘制、测量的时候被调用的 。具体来说,是在ViewRootImplperformTraversals方法中,会遍历DecorView中的子 View 并执行子 View 的dispatchAttachedToWindow方法 。这就像是一场精心安排的演出,在合适的时机,那些被缓存的任务就会被拿出来执行。

3. 执行时机探究

结合前面提到的 View 绘制流程,我们可以更清楚地理解View.post()获取宽高的原理 。在 Activity 的onCreateonStart等方法执行时,View 还没有开始绘制,attachInfo为空,View.post()的任务被缓存 。当执行到onResume之后,ViewRootImplperformTraversals方法被调用,开始进行 View 的测量(measure)和布局(layout) 。在这个过程中,View会执行dispatchAttachedToWindow方法,attachInfo被赋值,之前缓存的View.post()任务被执行 。此时,View 已经完成了测量和布局,所以在View.post()Runnable中就可以获取到正确的宽高值了 。而且,这些任务的执行是在首帧绘制之前,确保了我们能及时获取到宽高用于后续的操作 。

对比其他获取宽高方法

除了view.post()方法,还有一些其他方法可以获取 View 的宽高 ,它们各有特点和适用场景 。

1. 监听布局变化(ViewTreeObserver.OnGlobalLayoutListener)

使用ViewTreeObserver.OnGlobalLayoutListener可以监听视图树的全局布局事件,当视图树的布局发生变化时会触发回调 。通过这种方式可以在视图布局完成后获取其宽高 。示例代码如下:


View view = findViewById(R.id.my_view);
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        // 移除监听,避免多次调用
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
        } else {
            view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
        }
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
        // 处理宽高数据
    }
});

这种方法的优点是简单直接,能确保在布局完成后获取宽高 。缺点是只要视图树的布局发生变化,回调就会被触发 。如果布局频繁变化,可能会导致不必要的性能开销 。而且它是在布局变化时触发,不一定是首次布局完成时,所以在某些场景下可能不太适用 。

2. 手动测量(view.measure ())

通过手动调用view.measure(int widthMeasureSpec, int heightMeasureSpec)方法来得到 View 的宽高 。需要根据 View 的LayoutParams情况,使用MeasureSpec.makeMeasureSpec(int size, int mode)拼接出measure()方法的参数 。例如,当 View 的宽高是精确值(如 100dp)时,可以这样测量:


int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();

当 View 的宽高是wrap_content时:


int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();

这种方法的优点是可以在需要的时候主动测量 View 的宽高 。缺点是使用起来相对复杂,需要根据不同的布局参数情况来构造MeasureSpec 。而且只适用于一次完成测量过程的 View,对于一些复杂的布局,如RelativeLayoutTextView以及使用weight属性的LinearLayout等,可能需要多次调用measure()方法才能完成测量,这种情况下手动测量就不太适用了 。