为什么View.post方法可以用来获取视图尺寸

文中的源代码版本为api23

之所以会出现这篇文章,是因为最近用到了这个方法。 最开始我用的是Activity.runOnUiThreadHandler.post这两个方法,但是发现获取到的视图尺寸为0,而只有View.post才能得到我想要的结果,这让我觉得自己对View.post这个方法的理解还不充分,因此写下这篇文章做个记录。

1 Handler.post

Handler应该是大家非常熟悉的类了,其post方法会将Runnable对象包装成一个Message对象,放到消息队列中等待被执行。对于该方法就不展开讲了。 在构造Handler的时候我们可以传入Looper对象来指定Handler在什么线程工作。如果使用默认构造函数,则取决于创建Handler时所在的线程。

2 Activity.runOnUiThread

public final void runOnUiThread(Runnable action) {
    //当前线程如果不是主线程则通过mHandler
    //将action抛到主线程消息队列中,等待被执行
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        //如果当前线程就是主线程
        //那么直接调用run方法执行
        action.run();
    }
}
复制代码

相对于使用Handler可以指定线程,Activity.runOnUiThread方法只能在主线程任务。同时该方法还做了判断,如果当前线程就是主线程,那么立刻执行Runnable.run方法执行任务。

3 View.post

我们先来看看该方法的源码

public boolean post(Runnable action) {
    //mAttachInfo是在触发View.dispatchAttachedToWindow
    //方法时被赋值,它由ViewRootImpl负责创建
    //ViewRootImpl以及其管理的所有View、ChildView都共享这一
    //AttachInfo实例
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        //mHandler也有ViewRootImpl负责创建
        //它的实际类型为ViewRootHandler
        //工作在主线程
        return attachInfo.mHandler.post(action);
    }
    //getRunQueue返回的实际类型是RunQueue
    ViewRootImpl.getRunQueue().post(action);
    return true;
}


//RunQueue.java
void post(Runnable action) {
    postDelayed(action, 0);
}
void postDelayed(Runnable action, long delayMillis) {
    //将action封装成一个HandlerAction对象
    HandlerAction handlerAction = new HandlerAction();
    handlerAction.action = action;
    handlerAction.delay = delayMillis;

    synchronized (mActions) {
        //mActions实际类型为ArrayList<HandlerAction>
        mActions.add(handlerAction);
    }
}

复制代码

可以看到View.post方法有两个分支

  1. mAttachInfo字段不为空,则使用一个Handler直接将任务抛到主线程任务队列中等待执行
  2. mAttachInfo字段为空,则将任务保存在RunQueue

我们先不讨论mAttachInfo何时为空何时不为空的问题。

RunQueue中的任务在什么时候会被执行呢? 答案在ViewRootImpl.performTraversals

private void performTraversals() {
    //...
    getRunQueue().executeActions(mAttachInfo.mHandler);
    //...
}

//RunQueue.java
//executeActions的逻辑很简单
//遍历mActions中的所有HandlerAction
//然后使用传入的Handler将这些任务
//抛到消息队列中
void executeActions(Handler handler) {
    synchronized (mActions) {
        final ArrayList<HandlerAction> actions = mActions;
        final int count = actions.size();
        for (int i = 0; i < count; i++) {
            final HandlerAction handlerAction = actions.get(i);
            handler.postDelayed(handlerAction.action, handlerAction.delay);
        }
        actions.clear();
    }
}

复制代码

performTraversals方法我们知道是视图树执行重绘的核心方法,平时我们在调用View.requestLayout方法后,最终都会触发该方法。该方法执行结束,那么所有视图的尺寸就差不多都出来了。若此时在该方法内post一个消息,那么当这个消息被执行时肯定是可以拿到视图尺寸的。

3.1 复盘

基于这样的理解,我们再回过头来解决本文要解决的核心问题:为什么View.post方法可以用来获取视图尺寸

通常我们再Activity.onCreate方法中使用View.post

public class DemoActivity extends Activity{
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.actv_demo);
        view.post(new Runnable(){
            public void run(){
                Log.d("size",view.getMeasuredHeight()+":"+view.getMeasuredWidth());
            }
        });
    }
}
复制代码

那么此时view中的的mAttachInfo是否有值呢?答案是否定的。

前面提到过mAttachInfo字段是在触发View.dispatchAttachedToWindow方法时被赋值的

//View.java
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    //...
}
复制代码

那么什么时候会触发dispatchAttachedToWindow呢?答案在ViewRootImpl.performTraversals中。

private void performTraversals() {
    //...
    //当前的ViewRootImpl首次执行performTraversals
    //时,mFirst为true
    if (mFirst) {
        //...
        //host为DecorView,其本质是FrameLayout的派生类
        //dispatchAttachedToWindow会一层层传递到
        //视图树最低部的视图
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        //...
    }else{
        //...
    }
    //...
    //无论是否是第一次
    //这句代码都会执行
    getRunQueue().executeActions(mAttachInfo.mHandler);
    //... 
}
复制代码

那么ViewRootImpl首次执行performTraversals方法(也即是ViewRootImpl.scheduleTraversals方法首次被调用)是在什么时候呢?

答案在ActivityThread.handleResumeActivity中。

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {
    //...
    //Activity.onResume方法在performResumeActivity
    //方法执行的过程中被触发
    ActivityClientRecord r = performResumeActivity(token, clearHide);

    if (r != null) {
        //...
        if (r.window == null && !a.mFinished && willBeVisible) {
            //...
            if (a.mVisibleFromClient) {
                a.mWindowAdded = true;
                //第一次触发scheduleTraversals
                //wm的实际类型为WindowManagerImpl
                wm.addView(decor, l);
            }

        // If the window has already been added, but during resume
        // we started another activity, then don't yet make the
        // window visible.
        } else if (!willBeVisible) {
            //...
        }
        //...
    }else{
        //...
    }
}
复制代码

handleResumeActivity方法最终会通过WindowManager.addView触发scheduleTraversals,这个调用流程会比较长: WindowManagerImpl.addView-> WindowManagerGlobal.addView-> ViewRootImpl.setView-> ViewRootImpl.requestLayout-> ViewRootImpl.scheduleTraversals

scheduleTraversals会在主线程消息队列中放一个任务,当这个任务执行时就会触发performTraversals

结论是View.mAttachInfo这个字段是在Activity.onResume执行完毕之后在未来的某个消息周期才会被赋值的,也就是说,当我们在Activity.onCreate中调用View.post时,任务会被放到RunQueue中。

因此,我们可以使用View.post方法在Activity.onCreate中获取视图尺寸。

4 总结

还是上张图会比较清晰

Handler.post方法获取视图尺寸的操作发生在视图尺寸计算操作之前,因此获取到的视图尺寸为0。

在主线程中调用 Activity.runOnUiThread执行任务,使得获取视图尺寸的操作立刻被执行,遭遇视图尺寸计算,因此获取到的视图尺寸也是0。

View.post由于获取视图尺寸的时机在视图尺寸计算之后,因此可以正常获取到视图尺寸。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值