Bezier | 作者
承香墨影 | 编辑
https://juejin.cn/post/6865625913309511687 | 原文
郭霖(ID:guolin_blog) | 转自
前言
Hi,大家好,这里是承香墨影!
说到 Android 视图,大家第一反应肯定是 Activity 以及 View ,毕竟这是我们从入门开始,接触最多的两个组件。
但提到 Activity 和 View 之间联系以及设计背景,可能会难倒一大片人。
其实关于视图系统,还有一个重要概念 Window 不那么经常被提起,Android 的设计者,为了让开发者更容易上手,基于「迪米特法则」将 Window 屏蔽在内部。本文将从设计背景为出发点,阐述 Activity、Window、View 的实现流程,帮你换一种角度看 Android 视图系统,相信读完会让你耳目一新。
一、设计背景
1.1 View 存在的意义
View 直译过来就是视图的意思,是用来展示各种各样的视图。
关于 View 的绘制流程,相信大家都略知一二。共分为 measure、layout、draw,前两步用来决定宽高和位置,真正绘制视图是在 draw 过程,通过 onDraw()
方法传入的 Canvas 可以绘制各种各样的图形,而 Canvas 内部会通过 JNI 调用底层来实现真正的绘制操作。
既然 Canvas 就可以完成绘制,那 View 存在的意义是什么呢?
想绘制出五彩斑斓的效果,光有 Canvas 还远远不够。它还得配合 Paint、Matrix 等,这一系列操作让本就不简单的 Canvas 上手难度增高,复用率降低,绘制各种复杂界面,几乎成了不可完成的任务。
面对这种痛点 Android 系统通过「模板设计模式」,封装了一个用来管理绘制的组件 View,屏蔽大量细节的同时,提供三个模板方法 measure()
、layout()
、draw()
,开发者可以通过 View 的三大模板方法,自行定义视图的宽高、位置、形状,解决了大量模板代码,以及复用率低的问题。
一个复杂的界面,通常会包含很多元素比如:文字、图片等,根据「单一设计原则」Android 将其封装为 TextView、ImageView。
看起来万事大吉,但摆放这些 View 的时候又是一个大工程,各种坐标计算,不一会就晕头转向的,实际上摆放规则无非就那几种,所以 Android 利用 View 的 layout 特性封装了 RelativeLayout、LinearLayout 等 layout,用来控制各 View 之间的位置关系,进一步提升开发者效率。
1.2 如何管理错综复杂的 View?
通过自定义 View,可以绘制出我们任意想要的效果,一切看似很美好。
正当你盯着屏幕欣赏自己的作品时,"啪" 糊上来一个界面,一通分析得知,原来其他 App 也通过 View 操控了屏幕,你也不甘示弱通过相同操作,重新竞争到屏幕,如此反复进行的不可开交时,屏幕黑了。得,还是换回塞班系统吧~~~
玩笑归玩笑,回归到问题本身。由于对 View 的管理不当,造成了屏幕很混乱的情况。按常理来讲,当用户在操作一个 App 时,肯定不希望其他 App 蹦出来,所以在此背景下,急需一套「机制」来管理错综复杂的 View。
于是 Android 在系统进程中,创建了一个系统服务 WindowManagerService(WMS),专门用来管理屏幕上的「窗口」,而 View 只能显示在对应的窗口上,如果不符合规定,就不开辟「窗口」进而对应的 View 也无法显示。
为什么WMS需要运行在系统进程?
由于每个 App 都对应一个进程,想要管理所有的应用进程, WMS 需要找一个合适的地方,能凌驾于所有应用进程之上,系统进程是最合适的选择。
1.3 如何优雅的衔接各窗口之间关系?
自定义 View 可以定制各种视图效果,「窗口」可以让 View 有条不紊的显示,一切又美好了起来。
但问题又来了,每个 App 都会有很多个「界面(窗口)」,仅靠「窗口/View」来控制窗口和视图会面临一个很致命的问题:"不具备跳转和回退功能"。
按照我们的惯性思维,界面即可以跳转又可以回退,比如界面 A 跳转到 界面 B,按返回键理应退到界面 A,这是一个标准的栈结构。
但关于界面的启动以及「返回键」的监听似乎与「窗口」不太搭嘎,所以 Android 基于「单一设计原则」封装了 Activity,赋予窗口启动和回退功能,并由 AMS 统一调度 Activity 栈和生命周期。
又通过「迪米特法则」将「窗口」的管理屏蔽在内部,并暴露出 onCreate、onStart、onResume ... 等模版方法, 让开发者只专注于视图排版(View)和生命周期,无需关心「窗口」以及「栈结构」的存在。
所以,单纯说通过 Activity 创建一个界面,似乎又不那么准确,一切「窗口」均源自于 WMS ,而「窗口」中内容由 View 进行填充,Activity 只是在内部 "间接" 通过 WMS 管理窗口并协调好「窗口」与 View 的关系,最后再赋予栈结构、生命周期等功能而已。
Tips:
任务栈、返回栈并不是由 Activity 管理, Activity 只是提供了任务栈、返回栈的必要条件,出入栈管理由 AMS 承担。
关于 Activity 如何管理「窗口/View」? 请看第二小节。
二、实现流程
读源码的目的是为了理清设计流程,千万不要因果倒置,陷入到代码细节当中,所以要懂得挑重点,讲究点到为止。本文为了提供更好的阅读体验,会将源码中大部分无用信息删掉,只保留精华。
2.1 Activity 的由来
Activity 从何而来?想追溯到源头,恐怕要到从开天辟地时造就第一个受精卵开始。
开天辟地造就的 Zygote 从何而来
Android 系统会在开机时,由 Native 层创建第一个进程 init 进程 ,随后 init 进程会解析一个叫 init.rc
的本地文件,创建出 Zygote 进程。
字如其名,Zygote 的职责就是孵化进程。当孵化出的第一个进程 SystemServer 进程后退居幕后,通过 Socket 静静等待「创建进程」的呼唤,一切应用进程均由 Zygote 进程孵化。
SystemServer 进程的职责
SystemServer 是 Zygote 自动创建的进程,并且会长时间驻留在内存中,该进程内部会注册各种 Service。如:
ActivityManagerService(AMS):用来创建应用进程(通过 Socket IPC 通知 Zygote 进程)、管理四大组件;
WindowManagerService(WMS):用来开辟和管理屏幕上的「窗口」, 让视图有条不紊的显示;
InputManagerService(IMS):用来处理和分发各种「事件」;
等等...
为什么要将这些系统服务放在单独进程?
像 AMS、WMS、IMS 都是用来处理一些系统级别的任务,比如 Activity 存在「任务栈/返回栈」的概念,如果在通过 Activity 进行应用间跳转时,需要协调好「任务栈/返回栈」的关系,而不同应用又属于不同进程,所以需要一个地方能凌驾于所有应用进程之上,而单独进程是最好的选择。关于 WMS、IMS等其他Service同理 ,就不再赘述。
应用进程的创建过程
前面说到 AMS 可以通知 Zygote 进程孵化应用进程,那究竟何时「通知」呢?
其实大家应该已经猜到了,通过点击桌面上应用图标可以开启一个应用,所以 AMS 就是在此时通知 Zygote 创建应用进程。但「桌面」又是什么东西它从何而来?其实桌面也是一个 Activity, 它由 AMS 自动创建。
回归正题,点击应用图标到 Activity 的启动 这之间经历了什么流程?下面我简单列一下:
当点击一个 App 图标时,如果对应的应用进程还没有创建,则会通过 Binder IPC 通知到 AMS 创建应用进程;
应用进程启动后,会执行我们所熟悉的
main()
方法 ,而这个main()
方法,则位于 ActivityThread 这个类中,main()
方法对应的就是 Android 主线程;ActivityThread 的
main()
方法,首先会调用Looper.loop()
,用来循环处理主线程 Hanlder 分发的消息;接下来的
main()
方法,会发送一个BIND_APPLICATION
的消息, Looper 收到后会通过 Binder IPC,通知 AMS 创建 App进程 对应的 Application;Application 创建后,会再次通过 Binder IPC 通知 AMS 要创建 Activity ,AMS 验证后会回到 App 进程;
回到 App 进程后会间接调用
ActivityThread#performLaunchActivity()
来真正启动创建 Activity , 并且执行attach()
和onCreate()
;
Tips
Application 和 Activity 并不是通过 AMS 直接创建的, AMS 只是负责管理和验证,真正创建具体对象还得到 App 进程
Android 视图系统是一个很庞大的概念,几乎贯穿了整个 Java Framework ,由于 作者能力以及篇幅的原因,无法一文将 Java Framework 讲解清楚。所以就描述式的说了下系统进程、应用进程以及 Activity 的由来,尽可能你更清晰的认识 Android 视图系统。
2.2 PhoneWindow 不等价于 "Window(窗口)"
我之所以第一小节没有将「窗口」描述成 Window 是怕大家将二者混淆。因为应用进程的 Window/PhoneWindow 和真正的「窗口」根本就是两个概念。作者也曾在阅读源码时就这个问题困惑了很久。在此非常感谢「一只修仙的猿」在《Android 全面解析之 Window 机制》一文中给了我答案。
Android SDK 中的 Window 是一个抽象类,它有一个唯一实现类 PhoneWindow,PhoneWindow 内部会持有一个 DecorView(根View) ,它的职责就是对 DecorView 做一些标准化的处理,比如标题、背景、导航栏、事件中转等,很显然与我们前面所说的「窗口」概念不符合。
那 PhoneWindow 何时被创建?
2.1 小节我提到可以通过 ActivityThread#performLaunchActivity()
创建 Activity ,来看下其代码:
#ActivityThread
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
Activity activity = null;
//注释1
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
...
if (activity != null) {
...
//注释2.
activity.attach(...);
...
//注释3.
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
}
...
return activity;
}
首先通过「注释1」处创建一个 Activity 对象,然后在「注释2」处执行其 attach(..)
方法,最后在通过 callActivityOnCreate()
执行 Activity 的 onCreate()
方法。
先来看 attach()
做了什么事情:
#Activity
final void attach(...){
...
mWindow = new PhoneWindow(this, window, activityConfigCallback);
...
mWindow.setWindowManager(...);
mWindowManager = mWindow.getWindowManager();
...
}
Activity 会在 attach()
方法中创建一个 PhoneWindow 对象并复制给成员变量 mWindow
, 随后执行 WindowManager 的 setter、getter 。来重点看一下 setter 方法:
#Window
public void setWindowManager(...) {
...
if (wm == null) {
//注释1
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
//注释2
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
「注释1」处会通过系统服务获取一个 WindowManager 类型对象,用来管理 Window。
「注释2」会通过 WindowManager 创建一个 WindowManagerImpl 对象,实际上 WindowManager 是一个接口,它继承自 ViewManager 接口,而 WindowManagerImpl 是它的一个实现类。
绕来绕去原来是通过 WindowManager 创建了另一个 WindowManager ,看起来多此一举,那 Android 为什么要这样设计呢?
首先 WindowManager 具备两个职责,管理 Window 和 创建WindowManager 。系统服务获取的 WindowManager 具备创建 Window 功能,但此时并未与任何 Window 关联。而通过 createLocalWindowManager()
创建的 WindowManager 会与对应的 Window 一对一绑定。所以前者用于创建 WindowManager,后者用于与 Window 一对一绑定,二者职责明确,但让作者费解的是为什么不基于「单一设计原则」把创建过程抽取至另一个类?如果有知道的同学可以评论区留言,事先谢过~
关于 WindowManagerImpl 如何管理 Window 先暂且不提,下面文章会说到。
PhoneWindow 已经创建完毕,但还没有跟 Activity/View 做任何关联。扒一扒 PhoneWindow 的源码你会发现,它内部只是设置了标题、背景以及事件的中转等工作,与「窗口」完全不搭嘎,所以切勿将二者混淆。
2.3 DecorView 的创建时机
通过 2.2 可知 Activity 的 attach()
运行完毕后会执行 onCreate()
,通常我们需要在 onCreate()
中执行 setContentView()
才能显示的 XML Layout 。关于 setContentView()
顾名思义就是设置我们的 Content View 嘛。内部代码如下:
#Activity
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
...
}
public Window getWindow() {
return mWindow;
}
首先通过 getWindow()
获取到 attach()
阶段创建的 PhoneWindow ,随后将 layoutResID(XML Layout) 传递进去,继续跟:
#PhoneWindow
ViewGroup mContentParent;
public void setContentView(int layoutResID) {
//注释1
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
...
} else {
//注释2
mLayoutInflater.inflate(layoutResID, mContentParent);
}
}
「注释1」处会判断 mContentParent
是否为空,如果为空会通过 installDecor()
对其实例化,否则移除所有子 View。
「注释2」处会将 layoutResID
对应的 XML 加载到 mContentParent
。到此为止唯一的疑问是 mContentParent
如何被创建的,跟一下 installDecor()
:
#PhoneWindow
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor(-1);
...
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
...
}
}
首先创建 DecorView 类型对象并赋值给引用 mDecor
。那什么是 DecorView?
DecorView 继承自 FrameLayout,内部有一个垂直布局的 LinearLayout 用来摆放状态栏背景、TitleBar、ContentView、导航栏背景,其中 ContentView 就是用来存放由 Activity#setContentView()
传入的 Layout 。之所以设计出 DecorView,是因为部分场景状态栏背景、标题栏、导航栏背景等 需要系统统一管理。最后基于「迪米特法则」将其管理操作屏蔽在内部,只暴露出 ContentView 由开发者填充。
注意,DecorView 只是给状态栏、导航栏填充了背景,电量、信号..由系统统一绘制。
再回到 mDecor
的创建过程,跟一下 generateDecor(-1)
代码:
#PhoneWindow
protected DecorView generateDecor(int featureId) {
...
return new DecorView(context, featureId, this, getAttributes());
}
直接 new 出来了一个 DecorView。再回到我们最初的疑问, mContentParent
从何而来?installDecor()
创建出 DecorView 会通过 generateLayout(mDecor)
创建 mContentParent
。generateLayout(mDecor)
代码很长就不贴了,内部会通过 mDecor
获取到 mContentParent
并为其设置主题、背景等 。
到此阶段 DecorView 创建完毕并与 XML Layout 建立了关联,但此时根 View(DecorView)
还未与窗口建立关联,所以是看不到的。
为什么要在onCreate执行setContentView?
通过 setContentView()
可以创建 DecorView ,而一个 Activity 通常只有一个 DecorView(撇去Dialog等),如若将 setContentView()
放在 start、resume 可能会创建多个 DecorView , 进而会造成浪费。所以 onCreate()
是创建 DecorView 的最佳时机。
2.4 ViewRootImpl 如何协调 View 和 Window 的关系?
Activity 启动后会在不同时机通过 ActivityThread 调用对应的「生命周期」方法,onResume()
是一个特殊的时机它通过 ActivityThread#handleResumeActivity()
被调用。
代码如下:
#PhoneWindow
public void handleResumeActivity(...) {
//注释1
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
...
final Activity a = r.activity;
...
//注释2
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
...
//注释3
wm.addView(decor, l);
...
}
「注释 1」处会间接调用 Activity 的
onResume()
方法;「注释 2」处通过 Activity 获取 PhoneWindow、DecorView、WindowManager,它们的创建时机前面小结有写,忘记的可以回翻阅读;
「注释 3」处 调用了 WindowManager 的
addView()
方法,顾名思义就是将 DecorView 添加至 Window 当中,这一步非常关键;
关于 WindowManager 的概念 2.2 小结提到过,它是一个接口有一个实现类 WindowManagerImp,跟一下其 addView()
方法。
#WindowManagerImp
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
public void addView(...) {
...
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow, mContext.getUserId());
...
}
内部调用了 mGlobal
的 addView()
方法,其实不光 addView()
几乎所有 WindowManager 方法,都是通过委托 mGlobal
去实现,这种写法看似很奇怪,但实际上这种设计不仅不奇怪而且还很精妙,具体精妙在何处?我列出以下三点:
WindowManager 提供的功能全局通用不会与某个 View/Window 单独绑定,为了节省内存理应设计出一个「单例模式」;
WindowManagerImp 具备多个职责如 Token 管理、WindowManager功能 等,所以通过「单一设计原则」将 WindowManager 功能,拆分到另一个类中即 WindowManagerGlobal,并将其定义为单例;
为了不违背「迪米特法则」,又通过组合模式将 WindowManagerGlobal 屏蔽在内部;
回归正题,来看 mGlobal
的 addView()
方法:
#WindowManagerGlobal
/**
* 用来存储所有的DecorView
*/
private final ArrayList<View> mViews = new ArrayList<View>();
/**
* 用来存储所有的ViewRootImpl
*/
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
/**
* 用来存储所有的LayoutParams
*/
private final ArrayList<WindowManager.LayoutParams> mParams =
new ArrayList<WindowManager.LayoutParams>();
public void addView(...) {
...
ViewRootImpl root;
synchronized (mLock) {
root = new ViewRootImpl(view.getContext(), display);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
...
root.setView(view, wparams, panelParentView, userId);
...
}
}
首先创建一个 ViewRootImpl 类型对象 root ,然后将 view、root、wparams 加入到对应的集合,由 WindowManagerGlobal 的单例对象统一管理,最后执行 root 的 setView()
。根据我多年阅读源码的经验 答案应该就在 root.setView()
里,继续跟。
ViewRootImpl
public void setView(...) {
synchronized (this) {
if (mView == null) {
...
mView = view;
...
//注释1
requestLayout();
//注释2
res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mDisplayCutout, inputChannel,
mTempInsets, mTempControls);
...
//注释3
view.assignParent(this);
}
}
}
void assignParent(ViewParent parent) {
if (mParent == null) {
mParent = parent;
} else if (parent == null) {
mParent = null;
}
...
}
ViewRootImpl#setView()
方法很长,我做了下精简列出几个关键步骤:
「注释 1」
requestLayout()
通过一系列调用链最终会开启mView(DecorView)
的绘制(measure、layout、draw)。这一流程很复杂,由于篇幅原因本文就不提了,感兴趣的可查阅 Choreographer 相关知识;「注释 2」
mWindowSession
是一个 IWindowSession 类型的 AIDL 文件,它会通过 Binder IPC 通知 WMS 在屏幕上开辟一个窗口,关于 WMS 的实现流程也非常庞大,我们点到为止。这一步执行完我们的 View 就可以显示到屏幕上了;「注释 3」最后一步执行了
View#assignParent()
,内部将mParent
设置为 ViewRootImpl 。所以,虽然 ViewRootImpl 不是一个 View , 但它是所有 View 的顶层 Parent;
小结开头我有提到,好多人将 API 中的 Window/PhoneWindow 等价于「窗口」,但实际上操作开辟「窗口」的是 ViewRootImpl,并且负责管理 View 的绘制,是整个视图系统最关键的一环。
疑惑:
经常听到有人说
onStart()
阶段处于可见模式,对此我感到疑惑。通过源码的分析可知onResume()
执行完毕后才会创建 窗口 并开启 DecorView 的绘制,所以在onStart()
连窗口都没有何谈可见?
注意点:
初学 Android 时经常在
onCreate()
时机,获取 View 宽高而犯错,原因是 View 是在onResume()
后才开始绘制,所以在此之前无法获取到 View 宽高状态,此时可以通过View.post{}
或者addOnGlobalLayoutListener()
来获取宽高。
Java Framework 层面视图系统的实现非常复杂,为了方便大家理解,我列出提到的几个关键类和对应的职责。
Window 是一个抽象类,通过控制 DecorView 提供了一些标准的 UI 方案,比如 背景、标题、虚拟按键等;
PhoneWindow 是 Window 的唯一实现类,完善了 Window 的功能,并提供了 事件 的中转;
WindowManager 是一个接口,继承自 ViewManager 接口,提供了 View 的基本操作方法;
WindowManagerImp 实现了 WindowManager 接口,内部通过 组合 方式持有 WindowManagerGlobal ,用来操作 View;
WindowManagerGlobal 是一个全局单例,内部可以通过 ViewRootImpl 将 View 添加至「窗口」中;
ViewRootImpl 是所有 View 的 Parent ,用来管理 View 的绘制以及「窗口」的开辟;
IWindowSession 是 IWindowSession 类型的 AIDL 接口,可以通过 Binder IPC 通知 WMS 开辟窗口;
至此关于 Java Framework 层面视图系统的设计与实现梳理完毕
综上所述
一切视图均由 Canvas 而来;
View 的出现是为了提供视图「模板」,用来提升开发效率;
「窗口」可以让 View 有条不紊的显示;
Activity 给每个 窗口 增加生命周期,让「窗口」切换更加优雅;
PhoneWindow 只是提供些标准的 UI 方案,与「窗口」不等价;
可通过 WindowManager 将 View 添加到「窗口」;
ViewRootImpl 才是开辟窗口的那个角色,并管理 View 的绘制,是视图系统最关键的一环;
错综复杂的视图系统基本都隐藏 Activity 内部,开发者只需基于模板方法即可开发;
references:
Android 全面解析之 Window 机制 https://juejin.cn/post/6888688477714841608
Android 世界中,谁喊醒了 Zygote ?https://juejin.cn/post/6844903967340625933
-- End --
本文对你有帮助吗?留言、转发、点好看是最大的支持,谢谢!
推荐阅读: