聊聊为什么在activity启动的时候获取不到View的宽高


周五一大早看到一篇鸿洋推了一篇博客讲onResume中Handler.post(Runnable)为什么获取不到宽高?,细细读了两遍,有些收获,但依然有些不明白的地方,同时对有些细节不太认同。于是自己梳理了一下,在翻看源码后我得出了一个这样的结论。

在Activty启动的相关生命周期中提交到MainLooper的Message会在整个视图树注册时钟信号(垂直同步)之前处理,而且整个视图树会在注册后获得时钟信号的时候才去递归遍历进行测量和布局。

PS:
1.由于文中包含对阅读源码后的一些推测,可能有误,欢迎指出
2.这篇文章涉及的源码比较多,很多地方我都没贴源码。没看过Activity启动源码和setCotentView源码阅读本文可能引起不适。

Activity的启动和视图树的创建

关于Activity的启动我们从ActivityThread中的Handler收到启动消息LAUNCH_ACTIVITY说起。这一块就带大家看源码了,建议大家自己打开源码按着以下顺序自行阅读了解整个流程,这样能加深印象。

简单概括一下我们需要关注的整个流程。在handleLaunchActivity方法中调用performLaunchActivity创建并启动Activity,接着调用handleResumeActivity方法,该方法最终会回调ActivityonResume方法,在回调了onResume之后,会调用WindowMangeraddView方法把decorView添加进去以此构建整个视图树。

先来回顾一下,decorView是在哪里创建的?在onCreate方法中调用setContentView就会构建decorView。但这个时候这个顶层容器还没有被绘制到屏幕上。

贴一下自己画的时序图。

聊聊为什么在activity启动的时候获取不到View的宽高_第1张图片
Activity启动流程

思考一下,不管在onCreate还是onResume甚至是onStart,发送给MainLooper的消息都会在执行完LAUNCH_ACTIVITY之后才得到处理。通过上面的时序图可以看到,在回调了onResume之后,decorView也已经通过WindowManger被添加进了ViewRootImpl。这个时候依然获取不到控件宽高,说明还没有执行整个视图的绘制流程。可是组件的启动已经处理完了,这里我理解为这个Activity已经启动了并且用户可见了,但这时候还没绘制视图的话什么时候绘制呢?

这就要说说垂直同步信号了。

Android系统每隔16ms会发出VSYNC信号绘制界面。如果我们在16ms内没有完成绘制,就会展示上一帧的画面,画面就出现了掉帧。我简单查了一下,这个信号是由native层的核心服务SurfaceFlinger发出的。

聊聊为什么在activity启动的时候获取不到View的宽高_第2张图片
垂直同步信号

当Java层收到垂直同步信号,会导致整个视图树的绘制,只有当整个视图树绘制完毕后,我们才能获取控件的宽高。

那这个时候问题又来了,垂直信号是如何通知视图树去绘制的?


ViewRootImpl#requestLayout到底干了什么

通过上面的时序图,我们已经了解到最后会构建一个ViewRootImpl对象并把decorView添加进去。接下来我们看看垂直信号是如何和视图树的绘制关联到一起的,先看一下时序图。

聊聊为什么在activity启动的时候获取不到View的宽高_第3张图片
View的绘制流程

这里带着大家看看代码。

ViewRootImpl.class

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

MessageQueue.class

    public int postSyncBarrier() {
        return postSyncBarrier(SystemClock.uptimeMillis());
    }

    private int postSyncBarrier(long when) {
        // Enqueue a new sync barrier token.
        // We don't need to wake the queue because the purpose of a barrier is to stall it.
        synchronized (this) {
            final int token = mNextBarrierToken++;
            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            msg.arg1 = token;

            Message prev = null;
            Message p = mMessages;
            if (when != 0) {
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            if (prev != null) { // invariant: p == prev.next
                msg.next = p;
                prev.next = msg;
            } else {
                msg.next = p;
                mMessages = msg;
            }
            return token;
        }
    }

ViewRootImpl.class第4行调用了postSyncBarrier方法,这个方法在messageQueue插入了一条sync barrier消息。这个Message的特殊之处在于没有对应的处理消息的target,熟悉Handler机制的应该知道,你发送消息的时候,message中的target就是你发送消息的handler。因为该消息不依赖handler去发送消息,因此也理所应当不需要设置target

第5行调用了Choreographer对象postCallback方法,最后调用到了postCallbackDelayedInternal

这个Choreographer就是一个时钟信号的接收者和处理者。

在Choreographer中有三种消息

  • MSG_DO_FRAME:开始渲染下一帧的操作
  • MSG_DO_SCHEDULE_VSYNC:请求Vsync信号
  • MSG_DO_SCHEDULE_CALLBACK:请求执行callback

Choreographer.class

    private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            //这里把回调加入了队列
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
           
            if (dueTime <= now) {
                scheduleFrameLocked(now);
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);//设置为异步消息
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }

首先会把回调存入一个队列中,接着判断这个消息是否已经超时。根据我看源码的流程,这样走下来应该会dueTime是等于now的,也就是调用了scheduleFrameLocked方法。但我们还是先看看另外一半干了什么,调用setAsynchronous设置该消息为异步消息,并将之前的runnable作为回调。

这个方法的参数action就是上面传过来的mTraversalRunnable,而TraversalRunnableViewRootImpl的内部类。来看看它的实现。在其内部调用了doTraversal方法,这里就是整个视图树测量、布局的起点

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

接下来看看另外一半的逻辑,那scheduleFrameLocked干了什么呢?

scheduleFrameLocked中调用到了scheduleVsyncLocked方法。接着调用mDisplayEventReceiver.scheduleVsync(),这是一个native方法,入参是个句柄。我的理解是注册了一个垂直同步信号的监听器。当垂直同步信号发送过来的时候,会通过FrameDisplayEventReceiver#onVsync接收,接着发送消息到主线程,请求执行doFrame

    void doFrame(long frameTimeNanos, int frame) {
        //..省略大量代码
        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
            AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

            mFrameInfo.markInputHandlingStart();
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

            mFrameInfo.markAnimationsStart();
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } finally {
            AnimationUtils.unlockAnimationClock();
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

doFrame最后调用了doCallbacks,这个方法是把队列中的回调依次执行。还记得我们上面存入队列的TraversalRunnable么?至此当垂直信号到来的时候,就会最终回调到doTraversal遍历测量整个视图。


一些思考和补充

问题一

这个回调已经缓存在了Choreographer中,而每16ms的垂直同步信号为什么不会导致布局每16ms就测量一次呢??

答:(对于这个问题我有两个解释,不一定对)

  1. Choreographer执行完一次回调后,会回收所有的回调。关于这一点暂时没找到其他博客作为依据,只是看到在执行完回调后调用了recycleCallbackLocked方法。
  2. 调用view的requestLayout方法会修改mPrivateFlags标志位。而在 performLayout中会请求重新布局的view。如果这个标志位在一次绘制后设置为不请求绘制,则下次垂直信号来的时候并不会重绘这些布局。

问题二

上文说到接收到垂直同步信号后会发送消息到主线程请求执行doFrame,这个流程是怎么样的?

答:这里就要说回之前的sync barrier消息

MessageQueue.classnext方法中有这么一段代码。如果messagetargetnull,说明该messagesync barrier消息

    Message msg = mMessages;
        if (msg != null && msg.target == null) {
            // Stalled by a barrier.  Find the next asynchronous message in the queue.
            do {
                prevMsg = msg;
                msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }

当碰到这样一条消息的时候,就会轮询去找下一条异步消息。什么是异步消息呢?回到刚才接收垂直同步信号的地方,当FrameDisplayEventReceiver接收到垂直信号,会回调onVsync。上文我们提高过,Choreographer会通过FrameDisplayEventReceiver#onVsync接收,接着发送消息到主线程,请求执行doFrame。而这个消息会通过调用setAsynchronous被设置为异步消息。也就是说当我们的消息队列中有sync barrier消息才会执行这个异步消息

        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
            //...省略部分代码
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);//设置成异步消息在这里出现很多次了
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

最后上一张灵活画作帮助理解,帮助大家理解为什么onResume中Handler.post(Runnable)获取不到宽高。

聊聊为什么在activity启动的时候获取不到View的宽高_第4张图片
handler.jpg

参考资料

Android Choreographer 源码分析

你可能感兴趣的:(聊聊为什么在activity启动的时候获取不到View的宽高)