探究为何:在onCreate中通过View.post能获取宽高

惯例,导语:
最怕一生碌碌无为,还聊以自慰平淡是真。

探究为何:在onCreate中通过View.post能获取宽高_第1张图片

在之前的文章《Android解决在onCreate中获取View的width、Height为0的方法》提到过,可以通过View.post方式:

view.post(new Runnable() {
        @Override
        public void run() {
            view.getHeight(); //height可用
        }
    });

之后有同学问到:

探究为何:在onCreate中通过View.post能获取宽高_第2张图片

本着知其然知其所以然的学习态度,觉得还是有必要把为什么通过View.post方式就能获取到View的width/height的原理捯饬捯饬。

首先,观察View.post方法的实现:

public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action);
        }
        // Assume that post will succeed later
        ViewRootImpl.getRunQueue().post(action);
        return true;
    }

主要是根据attachInfo是否被初始化决定执行方式,那么attachInfo在Activity的onCreate()执行时到底是不是null呢?关于attachInfo的初始化,我们可以在View源码中找到,其只有在dispatchAttachedToWindow()方法才被赋值,而dispatchAttachedToWindow()方法的调用是来自于ViewGroup,继续向上层去找,我们就不得不追溯到ViewRootImpl的perFormTraversals()方法了,熟悉view流程的都知道,view的三大流程就是通过这个称为“执行遍历”的方法来完成的。但是这个方法有整整800行代码,就只取主要流程的代码了:

private void performTraversals() {
        // cache mView since it is used so much below...
        final View host = mView;
        if (mFirst) {
            ···
            host.dispatchAttachedToWindow(mAttachInfo, 0);
        } 
        ···
        //先于performMeasure被执行了
        getRunQueue().executeActions(attachInfo.mHandler);
        ...
        performMeasure();
        ...
        performLayout();
        ...
        performDraw();
 }

在这里,我们明确了attachInfo的初始化,在onCreate中执行View.post的时候,attachInfo还是null。回到post的代码,确认执行的是 ViewRootImpl.getRunQueue().post(action) 的逻辑:

static final class RunQueue {
        void post(Runnable action) {
            postDelayed(action, 0);//没有延时
        }

        void postDelayed(Runnable action, long delayMillis) {
            HandlerAction handlerAction = new HandlerAction();
            handlerAction.action = action;
            handlerAction.delay = delayMillis;

            synchronized (mActions) {
                mActions.add(handlerAction);
            }
        }
    }

RunQueue只是将需要执行的runnable消息暂时做一个存储,并且此消息没有延时。在前面ViewRootImpl.performTraversals()方法中我有注释:

//先于performMeasure被执行了
        getRunQueue().executeActions(attachInfo.mHandler);
        ...
        performMeasure();
        ...
        performLayout();
        ...
        performDraw();

getRunQueue().executeActions()竟然先于performMeasure()执行了,这还了得吗?如果是这样的话,我们通过View.post()方式获取的应该是还没有测量过的宽高呀!

好吧,我们还要看一下RunQueue.executeActions()的实现:

    void executeActions(Handler handler) {
            synchronized (mActions) {
                final ArrayList 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();
            }
        }

这里面其实也是调用Handler去post我们的Runnable,而ViewRootImpl的Handler就是主线程的Handler,因此在performTraversals()被执行的Runnable其实是被主线程的Handler的post到执行队列里面了。这里说明下,Android的运行其实是一个消息驱动模式,不了解消息机制的也可以看我的另一篇《Android源码 从runOnUiThread聊聊消息机制》。
根据消息机制原理,我们需要等待主线程的Handler执行完当前的任务,才会去执行我们View.post的那个Runnable。
那么当前正在执行了什么任务呢?答案是TraversalRunnable,具体我们也要看ViewRootImpl的源码,里面有TraversalRunnable的定义:

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
}

void doTraversal() {
        if (mTraversalScheduled) {
            ···
            performTraversals();
            ···
        }
    }

关于TraversalRunnable的调度时机,不再此篇范围了。
到这里,我能回答开篇有同学提到的问题了吧:

View.post(runnable)方法的代码会在view的draw方法之前调用么?

如果按照我们刚分析的performTraversals()方法的执行流程:

getRunQueue().executeActions(attachInfo.mHandler);
        ...
        performMeasure();
        ...
        performLayout();
        ...
        performDraw();

那么答案是明确的:View.post(runnable)方法的代码会在view的draw方法之前调用。

但,这是真的吗?不是!

OMG! 为毛?我曾也天真的以为。

我还是去做了实验,结果:

探究为何:在onCreate中通过View.post能获取宽高_第3张图片
注意到了没?measure被执行了三次,layout被执行了两次,中间穿插了post的Runnable的执行结果,然后在第二次的layout之后才会去执行draw流程!

通过上面的分析,可以明确的是:第一次layout和第二次layout应该是两个不同的任务。因为在这中间已经有了View.post的Runnable的执行结果,所以有了结论是:一共有三个任务,第一次performTraversals、我们的Runnable、第二次performTraversals。

那么为什么会执行两次performTraversals呢?还是要回到performTraversal()方法中,取出与performDraw相关的代码:

           ......
    if (!cancelDraw && !newSurface) {
        if (!skipDraw || mReportNextDraw) {
            ......
            performDraw();
        }
    } else {
        if (viewVisibility == View.VISIBLE) {

            scheduleTraversals();
        } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
            for (int i = 0; i < mPendingTransitions.size(); ++i) {
                mPendingTransitions.get(i).endChangingAnimations();
            }
            mPendingTransitions.clear();
        }
    }
    ......

可以看出,当newSurface为真时,performTraversals函数并不会调用performDraw函数,而是调用scheduleTraversals函数,从而再次调用一次performTraversals函数,从而再次进行一次测量,布局和绘制过程。

到这里终于有了明确答案了:

View.post(runnable)方法的代码不会在view的draw方法之前调用。

但是Android系统设计时,为什么要将整个初始化过程设计成这样?为什么当Surface为新的时候,要推迟绘制,重新进行一轮初始化?

希望有经验的同学解惑啊,欢迎讨论。

探究为何:在onCreate中通过View.post能获取宽高_第4张图片

你可能感兴趣的:(Android)