二十、 从源码分析 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所在的类。

相关推荐
理想不理想v13 分钟前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
sszmvb12341 小时前
测试开发 | 电商业务性能测试: Jmeter 参数化功能实现注册登录的数据驱动
jmeter·面试·职场和发展
测试杂货铺1 小时前
外包干了2年,快要废了。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
王佑辉1 小时前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
真忒修斯之船1 小时前
大模型分布式训练并行技术(三)流水线并行
面试·llm·aigc
ZL不懂前端2 小时前
Content Security Policy (CSP)
前端·javascript·面试
测试界萧萧3 小时前
外包干了4年,技术退步太明显了。。。。。
自动化测试·软件测试·功能测试·程序人生·面试·职场和发展
百事老饼干3 小时前
Java[面试题]-真实面试
java·开发语言·面试
时差9534 小时前
【面试题】Hive 查询:如何查找用户连续三天登录的记录
大数据·数据库·hive·sql·面试·database
CXDNW6 小时前
【网络面试篇】HTTP(2)(笔记)——http、https、http1.1、http2.0
网络·笔记·http·面试·https·http2.0