Bootstrap

Android View绘制流程源码浅析(从源码角度理解为什么子线程不能更新 UI)

前言

在面试中有这两个问题经常会被提问:

  1. Android 子线程可以更新UI吗?为什么?
  2. Activity 的 onResume 中可以拿到 View 的宽高吗?

先不着急说答案,这两个问题主要考察了 Android View 的绘制流程,下面就跟随系统源码对 View 的绘制流程来一次解密。

绘制流程 – 出发点 Activity 的 setContentView

首先来回想一下我们是如何使用 Activity 的:

public class TestActivity extends Activity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.xxx);
    }
}

调用 setContentView 将布局 id 作为参数传入,Activity 就会把布局创建出来,即然是在 onCreate 方法中传入布局,那么就从 onCreate 入手开始分析。

绘制流程 – Activity 的 onCreate 触发时 View 的绘制流程是怎样的?

生命周期回调是如何触发的?

在探究 View 绘制时,首先要清楚 Activity 的生命周期是如何发生回调的。onCreate 是首先触发的,根据这个线索,去查看下 ActivityThread 类的源码,由于本文主要探究 View 的绘制流程,所以对于 ActivityThread 的源码不会过于深究,只需要知道一些关键的调用点即可。先看一下 Activity onCreate 是在哪里调用的:
ActivityThread 源码:
在这里插入图片描述
点进去查看 callActivityOnCreate 源码:
在这里插入图片描述
这里调用到了 Activity 的 performCreate 方法:
在这里插入图片描述
onCreate 生命周期方法最终在 Activity 的 performCreate 中调用,其实其他的生命周期方法同样都在 Activity 类中的对应的 performXXX 中调用。

onCreate 与 View 绘制流程

了解了生命周期的回调调用,再来看看 Activity 的初始化工作在哪里进行的。onCreate 是第一个生命周期,那么初始化工作应该在回调 onCreate 之前,回到 ActivityThread 类的 performLaunchActivity 方法:
在这里插入图片描述
Activity 的 attach 方法就是其做初始化工作的方法,进入 Activity 类看一下 attach 方法的源码:
在这里插入图片描述
上来就对其内部成员 mWindow 进行了初始化,看一下的 mWindow 的定义:
在这里插入图片描述
在这里插入图片描述
Window 是一个抽象类,从注释中也可以看出,Window 只有一个子类 PhoneWindow。

回过头看继续看Activity,attch 中将 mWindow 实例化为一个 PhoneWindow 对象。

到目前为止呢,源码流程还没有使用到我们调用 setContentView 传入的布局,接着我们直接点进去 setContentView 源码看一下他的逻辑:
在这里插入图片描述
其中又调用到了 getWindow().setContentView(layoutResID); getWindow 就是单纯的返回了 mWindow 对象,上面我们也看到了 mWindow 的初始化,实例化为了一个 PhoneWindow,接着看一下 PhoneWindow 的 setContentView 方法:
在这里插入图片描述
看一下 installDecor 的源码:
在这里插入图片描述
这里有一个 mDecor,是一个 DecorView 对象,DecorView 本质上是一个 FrameLayout 的子类:
在这里插入图片描述
点进去 generateDecor 方法跟一下源码:
在这里插入图片描述
最终是返回了一个 DecorView 对象,回到 installDecor 方法继续看:
在这里插入图片描述
点进去看一下 generateLayout 的源码:
在这里插入图片描述
在返回前,调用了 DecorView 的 onResourcesLoaded 方法,并且传入了一个布局,这个布局文件在 sdk 文件夹中也可以找到,内容如下:
在这里插入图片描述
再看一下 DecorView 的 onResourcesLoaded 方法a:
在这里插入图片描述
这个方法主要将传入的布局 id 解析成 View 并且添加到 DecorView 中第 0 个位置,并且宽高都是充满布局。
接着回过头继续看 generateLayout 方法剩下的代码:
在这里插入图片描述
在这里插入图片描述
最终是将 DecorView 的 content 部分,也就是 FrameLayout 返回去了,赋值给了 mContentParent。

那么到目前为止,DecorView 的布局大概是这样:
在这里插入图片描述
看完 generateLayout 的源码, 接着回到 installDecor 方法:
在这里插入图片描述
到这里 installDecor 方法就看完了,主要就是创建出了 DecorView 并且将系统布局加载。到这里为止,仍然没有用到 setContentView 传入的布局,回到 PhoneWindow 的 setContentView 方法:
在这里插入图片描述
注意这里的 inflate 调用,在 installDecor 方法中调用 generateLayout 时已经对 mContentParent 赋值,也就是说 mContentParent 就是 DecorView 布局中的 content 部分,一个空白的 FrameLayout。
inflate 的调用,就是将我们传入的布局 id 加载到这个 mContentParent,那么此时的 DecorView 就变成了这样:
在这里插入图片描述
源码跟踪到这里,目前 Activity 中的 mWindow 被初始化,再回调 activity 的 onCreate 方法中通过 setContentView 又将 DecorView 初始化并且加载了布局。但是,仅仅只是这些对象初始化了,并没有测量绘制。

绘制流程 – Activity onResume 与 View 绘制流程

我们都知道 onResume 被调用时,说明 activity 已经可见,根据这个线索,回到 ActivityThread 方法中,看一下 onResume 是如何被调用的:
在这里插入图片描述
在 ActivityThread 的 handleResumeActivity 调用了 performResumeActivity 方法,performResumeActivity 方法中又调用了 activity.performResume, performResume 中又调用了 Instrumentation.callActivityOnResume 最终触发了 onResume。这些调用不是重点就不截图一一展示了,点进去源码很容易看到。
handleResumeActivity 执行完 performResumeActivity 方法后,activity 的 onResume 已经触发回调,但是 performResumeActivity 还有剩下的代码需要继续执行,接着看剩下的源码:
在这里插入图片描述
下面的代码中有一句 addView 添加的是 decorView 对象,调用者是 wm,wm 来自于 activity.getWindowManager,来看一下 getWindowManager:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
WindowManager 是一个接口,需要找一下它在哪里初始化的,在 Activity 类中搜索一下很容易就在 attach 方法中发现了 :
在这里插入图片描述
点进去 setWindowManager 方法:
在这里插入图片描述
mWindowManager 是一个 WindowManagerImpl 对象,那么 ActivityThread 中调用的 wm.addView 就是调用了 WindowManagerImpl 的 addView,看一下 WindowManagerImpl.addView 源码:
(我的电脑上 这部分源码文件损坏了 看不到源码 所以从在线源码中截图了)
在这里插入图片描述
实际上 WindowManagerImpl 只是将操作转发给了 mGlobal,mGlobal 是一个单例对象:
在这里插入图片描述
接着看一下 WindowManagerGlobal 的 addView 操作:

在这里插入图片描述
这个方法的核心逻辑是初始化了一个 ViewRootImpl,并且调用了其 setView 方法。ViewRootImpl 是顶层视图,当我们调用 view 的 requestLayout 或者 invalidate 最终都会循环调用到 ViewRootImpl 中。
这里先记下这个细节:在前面分析 onCreate 调用流程时,DecorView 中的布局已经创建出来,而 ViewRootImpl 在 onResume 之后才会创建。

接着看一下 ViewRootImpl 的 setView 方法:
在这里插入图片描述
setView 的代码也很多,不过本文所述的是绘制流程,那么只需要知道 setView 中调用了 requestLayout 和 view.assignParent 方法即可。
setView 传入的 view 就是 DecorView,调用 view.assignParent 后 ViewRootImpl 成为了 DecorView 的父 View,那么当 DecorView 中触发 requestLayout 最终都会回调到 ViewRootImpl 的 requestLayout,线程检查就是在 ViewRootImpl 的 requestLayout 方法中进行,来看一下 requestLayout 的源码:
在这里插入图片描述
requestLayout 方法中首先 checkThread 检查线程,这里就跟本文开头的两个问题有关系了,先看一下 checkThread:
在这里插入图片描述
判断的并不是主线程,而是当前线程! 不过,在这里其实就是判断的主线程,因为分析的流程是 Activity 的 onResume 调用后走到的这里,而 mThread 又是在 ViewRootImpl 构造方法中初始化,mThread 就是主线程:
在这里插入图片描述
接着再回到 requestLayout 方法:
在这里插入图片描述
在这里插入图片描述
这里的 postCallback 实际上就是通过 Handler 发送了一个消息,源码简单就不截图展示了,看一下这个 mTraversalRunnable 做了什么:
在这里插入图片描述
在这里插入图片描述
performTraversals 方法特别特别特别的长,而且我也没有完全搞明白,只能标出跟绘制流程相关的方法了解绘制流程:
在这里插入图片描述
注意 performMeasure 方法:
在这里插入图片描述
调用了 mView 的测量,mView 在前面的 setView 方法中被赋值,也就是 ActivityThread 中的 DecorView。这样就一层层触发了界面上所有 View 的测量。
回到 performTraversals 方法接着往下看:
在这里插入图片描述
紧接着就调用了 performLayout:
在这里插入图片描述
和测量一样的道理就不多说了,再回到 performTraversals,当测量和布局都完成了就轮到绘制了:
在这里插入图片描述
源码分析到此,基本可以摸清文章开始的两个问题了

由 View 绘制流程引出的一些常见问题

onCreate 中子线程更新 UI 为什么不会报错

在回答这个问题之前先回想一下,View创建的时机。DecorView 在 onCreate 时就创建好了,那 ViewRootImpl 呢?在 onResume 之后才会报错,如果子线程更新UI执行的时机在 ViewRootImpl 未创建之前,那么更新UI触发的 requestLayout 只会回调到布局的根 ViewGroup 中,他们的 requestLayout 并没有检查线程这一操作。
而在 onResume 之后,ViewRootImpl 的 setView 方法对 DecorView 进行了 assignParent 操作,成为了顶层视图,之后再触发 requestLayout 会进行线程检查。

onResum 可以获取宽高吗

当然不行,为什么?如果要获取 View 的宽高那么 View 肯定是走完测量流程了,回想一下源码的流程,handleResumeActivity 中先触发了 onResume 的回调,然后才开始调用 addView 才会走到测量的流程,测量都不一定结束,何谈获取宽高呢?

查阅其他博客时发现的错误

我查阅这部分知识相关博客时,有的博客说在子线程创建 View 就可以在子线程更新 UI,代码大概就是这样:
在这里插入图片描述
大家不要被 子线程创建View就可以在子线程更新UI 这句话给误导,仔细想一下这段代码,button 虽然是在子线程创建的,但是它的 onClick 事件是在子线程吗?分明就是在主线程,只是设置点击事件的代码在子线程罢了。
改造一下这段代码,让它真正从子线程修改UI:
在这里插入图片描述
大家可能会去亲自试验一下这段代码,也许有一部分读者这么写是能够正常运行的,并且 setText 确实是在子线程执行更新UI的,不过这是因为默认开启了硬件加速,并且setText方法也有优化,改变文字不一定会触发 requestLayout 方法,可以在 AndroidManifest.xml 中关闭硬件加速再试试:
在这里插入图片描述
关闭硬件加速后,只要是在子线程触发 View 的 requestLayout 方法,是一定会报错的。

;