美文网首页Android知识Android开发Android技术知识
探究为何:在onCreate中通过View.post能获取宽高

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

作者: 齐小政 | 来源:发表于2017-01-14 16:45 被阅读798次

    笔者解惑原文,并对原文有所借鉴之处,特此声明,读者可一并阅读原文:链接

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

    平淡无为.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

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/muuebttx.html