前言
一般有些需求中,会出现需要我们在Activity启动中获取UI控件相关大小或者在界面绘制完成之后刷新数据
我们都知道,在UI绘制完成之后执行相应的操作时机最好,不会阻塞主线程导致卡顿或者UI控件参数获取失败
也许大家使用过或知道Handler(MainLooper)#Post和View#Post都是把Runnable封装成Message再push到主线程中的looper的MessageQueue中,会发现在Activity的生命周期中执行这两种方式效果不同,前者不满足我们的需求,而后者却能做到
接下来,本文就从Activity启动流程以及UI刷新和绘制流程原理以及消息循环机制、同步障碍机制来剖析
下面是一个获取UI控件大小为例子:
1 |
|
日志显示结果:
1 | onCreate init myCustomView width=0 |
从日志中可以看出:
- 在Activity可交互之前的生命周期中UI直接操作是失效的,即使通过handler把UI操纵任务post到onResume生命周期之后,也依然获失效,日志可以看到此时UI界面都没有绘制
- View#post会让runnable在该View完成了measure、layout、draw之后再执行,这个时候当然就可以获取到UI相关参数了
源码
先看下两者的源码实现:
handler#post
1 | public final boolean post(Runnable r) { |
代码简单,可以看到就是把runnable封装成Message然后加入当前Looper的MessageQueue队列中
再看下View#post
1 | public boolean post(Runnable action) { |
HandlerActionQueue.class
1 | // 实际也是通过handler来post到主线程 |
重点来了,通过源码调用发现最终都是通过handler#post方式来加入到主线程队列中,api调用一样为何效果不一样,下面就从如下几个知识点来分析:
- Activity生命周期启动流程
- Message消息发送和执行原理机制
- UI绘制刷新触发原理机制
- MessageQueue同步障碍机制
Activity启动流程
这个流程不清楚的,可以网上搜,一大堆。但这里讲的是,ApplicationThread收到AMS的scheduleLaunchActivity的Binder消息之后,所在的binder线程,会通过ActivityThread中的mH(Handler)来sendMessage
1 | private class ApplicationThread extends ApplicationThreadNative { |
mH(Handler)会把这个异步消息加入到MainLooper中MessageQueue,等到执行时候回调handleLaunchActivity
1 | public void handleMessage(Message msg) { |
handleLaunchActivity方法会执行很多方法,这个是入口,简单来说会创建Activity对象,调用其启动生命周期,attach、onCreate、onStart、onResume,以及添加到WindowManager中,重点看下本文中onResume生命周期是如何回调的。在Activity可见之后,紧接着就是要触发绘制界面了,会走到handleResumeActivity方法,会在performResumeActivity中调用activity的onResume方法
1 | final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { |
由此可见:从handleResumeActivity执行流程来看,到onResume调用的时候,Activity中的UI界面并没有经过measure、layout、draw等流程,所以直接在onResume或者之前的onCreate中执行UI操纵都是无用的,因为这个时候UI界面是不可见的,并没有进行绘制
那为何通过hander#post让UI操作执行发生在handleLaunchActivity这个Message之后,还是不行呢?
Message消息发送和执行原理机制这里就不阐述了,hander#post能让执行发生在handleLaunchActivity这个Message之后,就是因为这个Message循环机制原理,可以让任务通常依据Message加入的先后顺序依次执行,所以我们在onResume中push的Message,就会在handleLaunchActivity这个Message之后执行
但是为何onResume中使用hander#post还不能UI操作呢,我们猜测其实handleLaunchActivity之后还没有同步完成UI绘制,那么我们接下来一起分析下UI绘制刷新触发原理机制
UI绘制刷新触发原理机制
我们直接分析触发条件,从上文中的wm.addView开始:
WindowManager会通过其子类WindowManagerImpl来实现,其内部又通过WindowManagerGlobal的单实例来实现addView方法,源码如下
WindowManagerGlobal.class
1 | public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { |
在addView中,我们调用了ViewRootImpl#setView,具体源码如下:
ViewRootImpl.class
1 | public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { |
setView完成了上述几个重要步骤,我们来看看requestLayout的实现是如何触发刷新绘制的:
1 |
|
从上述代码可以发现在addView之后同步执行到requestLayout,再到scheduleTraversals中设置了同步障碍消息
这是个简单的阐述,我们来看下源码实现:
MessageQueue.class
1 | private int postSyncBarrier(long when) { |
MessageQueue同步障碍机制: 可以发现就是把一条Message(注意这个Message是没有设置target的)直接手动循环移动链表插入到合适time的Message之后的即可
然后消息队列是如何识别这个障碍消息的呢,我们可以看下Looper#loop循环中获取MessageQueue#next获取下一个message是如何实现的
1 | Message next() { |
可以看到scheduleTraversals中设置了同步障碍消息,就是相当于在MessageQueue中插入了一个Message,并且是在onResume之后插入的,所以在onResume中使用handler#post加入的消息会在同步障碍Message之前,会先被执行,这个时候依然没有刷新绘制界面,所以在onResume中handler#post发送的消息中进行UI操作是失效的
待消息队列查询到同步障碍Message时候,会等待下个异步Message(刷新Message)出现
那么为何View#post就可以了呢,再回过头来看下其源码:
View.class
1 | public boolean post(Runnable action) { |
由于在onResume中执行,这个时候ViewRootImpl还没有初始化(在WindowManager#addView中才初始化),而mAttachInfo是在ViewRootImpl构造函数中初始化的,此时mAttachInfo == null
从上文我们知道,此时的消息都被添加进了View里面的mRunQueue队列中
然后在dispatchAttachedToWindow中,我们通过mRunQueue.executeActions(info.mHandler)这段代码,把mRunQueue中任务全部push到主线程中
那这个方法dispatchAttachedToWindow什么会被调用,回顾上文中ViewRootImpl第一次收到Vsync同步刷新信号之后会执行performTraversals,这个函数内部做了个判断,当第一次mFirst时候会调用
1 | host.dispatchAttachedToWindow(mAttachInfo, 0); |
然后把全局mAttachInfo下发给所有子View,其源码如下:
View.class
1 | void dispatchAttachedToWindow(AttachInfo info, int visibility) { |
可以看到这个函数同时执行了
1 | mRunQueue.executeActions(info.mHandler); |
从上文可知就是通过hander把mRunQueue中任务全部push到主线程中。由此可以知道在performTraversals中push Message到主线中,肯定会这个performTraversals之后再执行,并且在doTraversals中移除了同步障碍消息,故会依次执行。所以onResume中View.post的Message就会在performTraversals之后执行,而performTraversals就是完成了View整个测量、布局和绘制
另外,当View的mAttachInfo != null时消息是直接post到主线程中的,但因为mAttachInfo不为空,也说明了肯定完成过UI绘制
总结
本文主要通过四个知识点,分析了Handler#Post和View#Post的UI效果差异原因
说明了为何Handler#Post无法保证在UI绘制后执行,而View#Post却可以
- Activity生命周期启动流程
- Message消息发送和执行原理机制
- UI绘制刷新触发原理机制
- MessageQueue同步障碍机制