前言
说到 Android 视图,大家第一反应肯定是 Activity 以及 View,毕竟这是我们从入门开始接触最多的两个组件。但提到 Activity 和 View 之间联系以及设计背景可能会难倒一大片人。其实关于视图系统还有一个重要概念 Window 不那么经常被提起,Android 的设计者为了让开发者更容易上手,基于迪米特法则将 Window 屏蔽在内部。本文将从设计背景为出发点阐述 Activity、Window、View 的实现流程,帮你换一种角度看 Android 视图系统。
1. 设计背景
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 时肯定不希望其他 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 的关系,最后再赋予栈、生命周期等功能而已。
关于 Activity 如何管理窗口/View?请看第二小节。
2. 实现流程
读源码的目的是为了理清设计流程,千万不要因果倒置陷入到代码细节当中,所以要懂得挑重点,讲究点到为止。本文为了提供更好的阅读体验,会将源码中大部分无用信息删掉,只保留精华。
2.1 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(),用来循环处理主线程 Handler 分发的消息。
接下来的 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 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 由开发者填充,符合迪米特法则。
再回到 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 内部,开发者只需基于模板方法即可开发。

