我们经常会遇到要获取 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消息机制之同步屏障