聊聊为什么在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启动流程

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

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

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

垂直同步信号

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

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


ViewRootImpl#requestLayout到底干了什么

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

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)获取不到宽高。

handler.jpg

参考资料

Android Choreographer 源码分析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容