概述
Activity,Window,View三者之间到底是什么关系? 如果长期只做CV工程师,那么很难了解到这个层面。答案并不复杂,只需要深入源码探索一番,探索的流程请看正文。
从Activity的setContentView开始
当我们创建一个新的布局,并把这布局指向一个Activity
时,通常的做法就是,新建一个Activity
,然后调用 setContentView(R.layout.my_layout);
,最后 startActivity
来启动这个新的Activity
。 平常写代码,都接触不到Window
这一层。也不知道 我们给出去的R.layout.my_layout
,是如何变成 新的Activity
上的那些View
实例。
可是只要我们点开Activity.java
中setContentView
的源代码:
从上面可以得知,Activity
只是将 layoutResId
交给了 一个 Window
类型的实例去处理的。Activity
本身并没有做任何事。
所以,看到这里,可以得出结论,setContenView
的实际操作,都委托给了 Window
的 setContentView()
插播解释 Window来源
我们在上一篇 juejin.cn/post/729814... 中,startActivity
最终会调用 ActivityThread
的 performLaunchActivity
方法,反射创建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的setView
将view
添加到WindowManager
中
- 用
ViewRootImpl的setView
- 图中1处,requestLayout执行之后,viewRoot所关联的view将会执行 测量布局绘制操作,确保添加到屏幕上之前,已经是一个绘制完成的View。
- 图中2处,调用 mWindowSession,将View添加到WindowManager中。
- windowSession是 WindowManagerGlobal中的单例 Binder 对象,用于本app进程与系统服务进行通信
在执行完了 mWindowSession.addToDisplay
之后,window
成功地传递到了WindowManagerService
,剩余的重点工作,就全部转移到了系统的WindowManagerService
中。
小结
通过 activity
的setContentView
的源码跟踪可以得出结论:
添加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()
的源代码如下:
最终调用了 WindowCallback
的 dispatchTouchEvent
方法。
这个 WindowCallback
实际上就是 Activity
类型。原因如下:
在 startActivity阶段,创建Activity对象,并调用了attach方法,其中,就有这么一句mWindow.setCallback(this)
, 这里的this,就是 Activity 对象。
那么触摸事件的传递,就到达了 Activit.java
的 dispathTouchEvent
。
上图可以看出,虽然名义上是由 Activity
在处理触摸事件,但是实际上还是交给了 PhoneWindow
,PhoneWindow
又交给了 DecorView
,执行的还是 DecorView
的 dispatchTouchEvent
.
总结
-
一个
Activity
中有一个Window
,也就是PhoneWindow
对象,PhoneWindow
中有一个DecorView
,setContentView
时,会将layout
填充到DecorView
中。 -
一个应用进程中只有一个
WindowManagerGlobal
对象,因为在ViewRootImpl
中它是静态的 -
每个
PhoneWindow
对应一个ViewRootImpl
,因为是new出来的 -
WindowManagerGlobal
通过调用ViewRootImpl
的setView
,完成 window的添加过程 -
ViewRootImpl
的setView
方法主要完成两件事,View
的渲染(requestLayout)
和 处理事件的接收传递。
TIPS
Q: 为什么引入某些第三方SDK的时候一定要同时引入 混淆规则?
A: 这是因为:androidFramework中很多地方使用了反射来创建对象, 比如,如果开启混淆,打release包。我们用到的一个第三方SDK中定义了一个XXXActivity,那么这个类就不能被混淆,否则,就会出现 start这个XXXActivity不成功的现象,报错内容为:找不到某某Activity。 再比如,第三方SDK中自定义了一个View,然后我们在 自己的布局文件中使用到了这个view,写了全类名,此时,如果没有将这个view的全类名写入到混淆规则中,那么也会报错,找不到这个View所在的类。