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