二十、 从源码分析 Window、Activity、View之间的关系

概述

Activity,Window,View三者之间到底是什么关系? 如果长期只做CV工程师,那么很难了解到这个层面。答案并不复杂,只需要深入源码探索一番,探索的流程请看正文。

从Activity的setContentView开始

当我们创建一个新的布局,并把这布局指向一个Activity时,通常的做法就是,新建一个Activity,然后调用 setContentView(R.layout.my_layout);,最后 startActivity来启动这个新的Activity。 平常写代码,都接触不到Window这一层。也不知道 我们给出去的R.layout.my_layout,是如何变成 新的Activity上的那些View实例。

可是只要我们点开Activity.javasetContentView的源代码:

从上面可以得知,Activity只是将 layoutResId交给了 一个 Window类型的实例去处理的。Activity本身并没有做任何事。

所以,看到这里,可以得出结论,setContenView的实际操作,都委托给了 WindowsetContentView()

插播解释 Window来源

我们在上一篇 juejin.cn/post/729814... 中,startActivity 最终会调用 ActivityThreadperformLaunchActivity方法,反射创建Activity对象,并执行它的 attach方法。

实际上,上面提到的Window只有一个实现类,那就是上图看到的PhoneWindow, 接下来,调用 mWindow.setWindowManager(),来将系统的WindowManager传给了mWindow.

获取系统WindowManager的方式为:

java 复制代码
(WindowManager)content.getSystemService(Context.WINDOW_SERVICE)

下图是 Window类setWindowManager()源码:

Window类在拿到系统WindowManager之后,利用它 createLocalWindowManager(this)创建了一个WindowManager对象,并放在了 Window对象中。

核心过程来到 PhoneWindow的setContentView

上图中,有两个关键点:

  • installDecor()

    • 如果 mContentView为空,就执行 installDecor(),确保 mContentView不为空,然后往下执行。
  • mLayoutInflater.inflate(layoutResId,mContentParent)

    • layoutResId这个布局文件最终交给了 mLayoutInflater,它将会根据 布局文件,反射生成一个 viewGroup实例,并将它挂到 mContentView中。

跟踪源代码到这里,可以得出结论:

新的 Activity 组成部分是

  • 一个最外层的 PhoneWindow,PhoneWindow内部自带一个 DecorView(FrameLayout类型)
  • DecorView内部包含一个 mContentView(ViewGroup)
  • 我们在Activity中做的setContentView(R.layout.my_layout)的动作,最终转换出了一个 ViewGroup实例,挂到了 mContentView中。

简而言之,就是 R.layout.my_layout -> mContentView子view

注意

Activity执行完onCreate之后并不是可见的。只有在执行完onResume之后才是可见且可交互的。因为: onCreate阶段只是创建了Activity要显示的内容,而在onResume阶段才会将 创建出来的内容绘制在屏幕上。 而onResume阶段,ActivityThread UI线程做的最重要的事情,就是下图所示 handleResumeActivity,源代码如下:

其中wm就是WindowManager,执行它的 addView方法,就是把 onCreate阶段创建出来的 decorView 添加到 windowManager上。

在这一步执行完毕之后

  • DecorView将会被渲染到屏幕上
  • DecorView可以接收屏幕触摸事件

接下来重点观察 WindowManager.addView() 具体做了什么.

WindowManager的addView

WindowManager是一个接口,它的唯一实现是 WindowManagerImpl , 下图是addView方法的内容。

上图中,mGlobal.addView,mGlobal的类型是 WindowManagerGLobal,mGlobal 是一个单例,每个app进程中仅有一个。

红框标记的关键代码解读:

  • root = new ViewRootImpl(view.getContent(),display);

    • 创建出一个 ViewRootImpl 对象
  • root.setView(view, wparams, panelParentView, userId);

    • ViewRootImpl的setViewview 添加到 WindowManager

ViewRootImpl的setView

  • 图中1处,requestLayout执行之后,viewRoot所关联的view将会执行 测量布局绘制操作,确保添加到屏幕上之前,已经是一个绘制完成的View。
  • 图中2处,调用 mWindowSession,将View添加到WindowManager中。
    • windowSession是 WindowManagerGlobal中的单例 Binder 对象,用于本app进程与系统服务进行通信

在执行完了 mWindowSession.addToDisplay之后,window成功地传递到了WindowManagerService,剩余的重点工作,就全部转移到了系统的WindowManagerService中。

小结

通过 activitysetContentView的源码跟踪可以得出结论:

添加View的操作 完全不在 Activity中,而是在PhoneWindow,在PhoneWindow背后,还有 WindowManagerService(WMS) 在全盘负责。 addView的过程中,Activity没什么参与感。

但是。在activity 执行完了 onResume之后, 屏幕如果接收到touch事件,这个touch首先是Activity感知到,然后往下分发给 WMS 创建出来的那些View

那么问题就来了,Touch事件是如何传递到 Activity的呢?

ViewRootImpl.setView

上图中,mWindowSession.addToDisplay执行完了,view就会出现在屏幕上,但是触摸事件的设置,是在上图红框内的代码中。这段代码,设置了输入事件的处理逻辑。

当手指碰到屏幕之后,第一时间收到触摸事件的是 硬件驱动层,驱动层通过socket通信,将事件告知 androidFrameWork层,其实也就是WMS,之后,touch事件会到达上图中的 input pipeline 输入管道中。

这些输入管道,其实是链表结构。

下图是 InputState的源代码:

touch事件会被 onProcess 捕获,然后最终会调用 图中2处的 mView.dispatchPointerEvent() ,mView实际上就是我们创建出来的 DecorView

View.dispatchPointerEvent() 的源代码如下:

最终调用了 WindowCallbackdispatchTouchEvent方法。

这个 WindowCallback 实际上就是 Activity类型。原因如下:

在 startActivity阶段,创建Activity对象,并调用了attach方法,其中,就有这么一句mWindow.setCallback(this), 这里的this,就是 Activity 对象。

那么触摸事件的传递,就到达了 Activit.javadispathTouchEvent

上图可以看出,虽然名义上是由 Activity在处理触摸事件,但是实际上还是交给了 PhoneWindowPhoneWindow 又交给了 DecorView,执行的还是 DecorViewdispatchTouchEvent.

总结

  1. 一个Activity中有一个Window,也就是 PhoneWindow 对象, PhoneWindow 中有一个DecorViewsetContentView时,会将 layout填充到 DecorView中。

  2. 一个应用进程中只有一个WindowManagerGlobal对象,因为在ViewRootImpl中它是静态的

  3. 每个PhoneWindow对应一个 ViewRootImpl,因为是new出来的

  4. WindowManagerGlobal通过调用 ViewRootImplsetView,完成 window的添加过程

  5. ViewRootImplsetView方法主要完成两件事,View的渲染(requestLayout) 和 处理事件的接收传递。

TIPS

Q: 为什么引入某些第三方SDK的时候一定要同时引入 混淆规则?

A: 这是因为:androidFramework中很多地方使用了反射来创建对象, 比如,如果开启混淆,打release包。我们用到的一个第三方SDK中定义了一个XXXActivity,那么这个类就不能被混淆,否则,就会出现 start这个XXXActivity不成功的现象,报错内容为:找不到某某Activity。 再比如,第三方SDK中自定义了一个View,然后我们在 自己的布局文件中使用到了这个view,写了全类名,此时,如果没有将这个view的全类名写入到混淆规则中,那么也会报错,找不到这个View所在的类。

相关推荐
Lee川1 小时前
优雅进化的JavaScript:从ES6+新特性看现代前端开发范式
javascript·面试
Lee川4 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i6 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
绝无仅有7 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有7 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫8 小时前
Looper.loop() 循环机制
面试
AAA梅狸猫8 小时前
Handler基本概念
面试
Wect8 小时前
浏览器缓存机制
前端·面试·浏览器
掘金安东尼9 小时前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试
掘金安东尼9 小时前
Next.js 企业级落地
前端·javascript·面试