面试官问我:View.post为什么能够获取View的宽高

194 阅读6分钟

记得看文章三部曲,点赞,评论,转发。 微信搜索【程序员小安】关注还在移动开发领域苟活的大龄程序员,“面试系列”文章将在公众号同步发布。

1.前言

最近看到几个技术群里都在吐槽目前面试八股文泛滥,作为技术担当的天才少年_决定要去各个大厂踢馆,用我扎实的八股文功底来教育一下他们。

2.正文

一路披荆斩棘,天才少年终于来到一个大厂的面试会议室。 是心动啊,糟糕眼神躲不掉,对你莫名的心跳,竟然停不了对你的迷恋~~

在这里插入图片描述

小伙子,我是今天的面试官,你不要对我抱有幻想,准备好了的话,咱们直接开始面试吧。

什么鬼,我的内心他怎么知道!

在Activity的oncreate方法中如何获取view的宽度和高度?

在这里插入图片描述

很简单,直接用view.post方法就可以获取到,代码如下所示:

view.post(new Runnable() {
            @Override
            public void run() {
                //获取宽度和高度
            }
        });

嗯,为什么一定要调用post方法呢?如果直接调用getHeight取到的值是多少呢?

嘿嘿,绕来绕去,不就是view.post源码的执行流程嘛,天才少年岂是浪得虚名,为了熟读framework源码,我抛弃了小琴,那是个风高夜黑的晚上,小琴拉着我的手问我愿不愿意做孩子的父亲,我反手就是一巴掌,怎么能让小孩来打破我的单身生活。思绪飘得有点远,我推了推眼镜,试图掩盖留下的泪水,清了清嗓子,准备开始讲(装)解(逼)。 在这里插入图片描述

先看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;
    }

代码量不大,可以很明显的看出来,attchInfo对象起到决定性作用,他是否为空,决定下面流程的运转,我先站在上帝的视角跟你说一下,attchInfo是在ViewRootImpl的构造函数中初始化,ViewRootImpl是在onResume之后才会创建,具体的源码,后面我会单独写一篇文章来讲解。 这边你只需要知道,在onCrate方法里面,attchInfo为空,则会走到ViewRootImpl.getRunQueue().post(action); RunQueue的源码如下所示:

static final class RunQueue {
        private final ArrayList<HandlerAction> mActions = new ArrayList<HandlerAction>();//post发送的action(Runnable)放入HandlerAction对象中,然后存入mActions集合中

        void post(Runnable action) {
            postDelayed(action, 0);
        }

        void postDelayed(Runnable action, long delayMillis) {
            HandlerAction handlerAction = new HandlerAction();//post发送的action(Runnable)放入HandlerAction对象中
            handlerAction.action = action;
            handlerAction.delay = delayMillis;

            synchronized (mActions) {
                mActions.add(handlerAction);//存入mActions集合中
            }
        }

        void removeCallbacks(Runnable action) {
            final HandlerAction handlerAction = new HandlerAction();
            handlerAction.action = action;

            synchronized (mActions) {
                final ArrayList<HandlerAction> actions = mActions;

                while (actions.remove(handlerAction)) {
                    // Keep going
                }
            }
        }

        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);//循环遍历mActions中的HandlerAction对象,把Runnable发送到该handler的messageQueue中。
                }

                actions.clear();
            }
        }

        private static class HandlerAction {
            Runnable action;
            long delay;

            @Override
            public boolean equals(Object o) {
                if (this == o) return true;
                if (o == null || getClass() != o.getClass()) return false;

                HandlerAction that = (HandlerAction) o;
                return !(action != null ? !action.equals(that.action) : that.action != null);

            }

            @Override
            public int hashCode() {
                int result = action != null ? action.hashCode() : 0;
                result = 31 * result + (int) (delay ^ (delay >>> 32));
                return result;
            }
        }
    }

这代码有点多,可以稍微解释一下吗?

别急啊,装逼不需要思考的吗? 核心代码我已经注释过了,相信很好理解:当调用ViewRootImpl.getRunQueue().post(action)时,该action被存储在RunQueue类中的集合 mActions中,等外部调用executeActions方法时,才会真正的执行action的回调。再一次借用上帝视角,这个handler其实就是UI线程的,所以这个时候才是真正的把action放到UI线程对应的MessageQueue中。

那是哪个外部调用executeActions方法的呢?可以讲一讲吗?

肯定可以啊,我来就是面试的,难不成来跟你相亲的。

在这里插入图片描述

我们知道View的渲染离不开ViewRootImpl类,页面绘制从requestLayout开始,那我们今天就从requestLayout方法往下讲,如果你想听requestLayout前面的东西,二面咱们细聊,又是一个勾引面试欲望的小技巧。

    @Override
    public void requestLayout() {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }

checkThread前面分析过,就是检查更新线程跟新建view的线程是否是用一个,跟今天要讲的不相关,暂时忽略,我们看scheduleTraversals()

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().postSyncBarrier();//设置同步屏障
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//通过mChoreographer垂直脉冲刷新页面,调用mTraversalRunnable
            scheduleConsumeBatchedInput();
        }
    }

可以看到这边设置了同步屏障,同步屏障和异步消息这个咱们后面单独用一篇文章来讲,这边你只需要知道,用同步屏障后,优先执行异步消息,而mChoreographer内部必然是异步消息,见下面的代码:

Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);//设置异步消息
mHandler.sendMessageAtTime(msg, dueTime);

继续看mTraversalRunnable的代码,如下所示:

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "performTraversals");
            try {
                performTraversals();
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

继续看performTraversals();

private void performTraversals() {
        // cache mView since it is used so much below...
        final View host = mView;

       //省略一些代码
        host.dispatchAttachedToWindow(attachInfo, 0);//**注意performMeasure,performLayout,performDraw在这个方法后面哦**
        mFitSystemWindowsInsets.set(mAttachInfo.mContentInsets);
        host.fitSystemWindows(mFitSystemWindowsInsets);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//测量
        performLayout(lp, mWidth, mHeight); // 布局
        performDraw();//绘制

可以看到在performTraversals方法中,调用了host.dispatchAttachedToWindow(attachInfo, 0),host其实View,那我们看下View的源码:

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        mAttachInfo = info; // 这个时候attachinfo才被赋值
      ... 省略部分代码
        // Transfer all pending runnables.
        if (mRunQueue != null) {
            mRunQueue.executeActions(info.mHandler); // 终于看到他了
            //executeActions方法会循环遍历mActions中的HandlerAction对象,把Runnable发送到该handler的messageQueue中。
            mRunQueue = null; 
        }
}

到这里基本真想大白了,调用view.post方法并不是实时调用,而是被存储在RunQueue类中的集合 mActions中,等外部调用executeActions方法,而executeActions是在View的dispatchAttachedToWindow方法中被调用。 时序图如下所示: 在这里插入图片描述

看你的分析,executeActions方法是在页面刷新方法(performMeasure,performLayout,performDraw)前面,明明页面刷新在调用之后,为什么view.post还能拿到view的宽和高呢?

可以啊,这都能看出来,不过我既然来了,就打算得到你,不是,就是回答你。 还记得我前面讲到同步屏障吗,怕你不记得,代码再贴给你看下

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().postSyncBarrier();//设置同步屏障
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//通过mChoreographer垂直脉冲刷新页面,调用mTraversalRunnable
            scheduleConsumeBatchedInput();
        }
    }

mChoreographer.postCallback其实也是一个Handler消息,并且是异步消息,上面已经讲过了,所以他会优先执行,而通过view.post发送的消息是同步消息,当设置了同步屏障后,会优先执行异步消息,所以必须等performMeasure,performLayout,performDraw流程走完后,view.post发送的消息才能真正的执行,自然也就可以拿到view的宽和高了。

可以了,你对handler,view,ViewRootImpl了解很深刻,今天有点晚了,消息屏障,view的创建流程,mChoreographer发送消息的流程,咱们后面再聊。


微信搜索【程序员小安】“面试系列(java&andriod)”文章将在公众号同步发布。