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

来源:互联网 发布:mac dps文件怎么打开 编辑:程序博客网 时间:2024/05/19 03:27

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

平淡无为.png

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

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

之后有同学问到:

question.png

本着知其然知其所以然的学习态度,觉得还是有必要把为什么通过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<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();            }        }

这里面其实也是调用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! 为毛?我曾也天真的以为。

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

textview.png
注意到了没?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为新的时候,要推迟绘制,重新进行一轮初始化?

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

together.jpeg

1 0