前言
说到Android视图
大家第一反应肯定是Activity
以及View
,毕竟这是我们从入门开始接触最多的两个组件。但提到Activity
和View
之间联系以及设计背景可能会难道一大片人。其实关于视图系统还有一个重要概念Window
不那么经常被提起,Android
的设计者为了让开发者更容易上手,基于迪米特法则
将Window
屏蔽在内部。本文将从设计背景为出发点阐述Activity、Window、View
的实现流程,帮你换一种角度看Android视图系统,相信读完会让你耳目一新。
1. 设计背景
1.1 五彩斑斓的效果皆源自Canvas
Android手机本质是一块屏幕,为了方便开发者绘制出五彩斑斓的效果,Android系统在Java Framework
封装了一块画布Canvas
,它配合Paint
、Matrix
几乎可以画出任意效果
但光有Canvas
还远远不够,因为它上手难度高、复用率低,绘制各种复杂界面几乎成了不可完成的任务。面对这种痛点Android系统通过模板设计模式
封装了一个用来管理绘制的组件View
,屏蔽大量细节的同时提供三个模板方法measure、layout、draw
,开发者可以通过View
的三大模板方法自行定义视图的宽高、位置、形状
,解决了大量模板代码以及复用率低的问题。
一个复杂的界面通常会包含很多元素比如文字、图片等
,根据单一设计原则
Android将其封装为TextView、ImageView
。看起来万事大吉,但摆放这些View
的时候又是一个大工程,各种坐标计算不 一会就晕头转向的,实际上摆放规则无非就那几种,所以Android
利用View
的layout
特性封装了RelativeLayout、LinearLayout
等layout
用来控制各View
之间的位置关系,进一步提升开发者效率。
所以View的出现是为了解决Canvas
使用难度高、复用率低的问题。仅就Java Framework
来讲:“Canvas 可以没有 View,但 View 不能没有 Canvas。
”,归根到底View
只是视图排版工具。而ViewGroup
则是View
的排版工具
引号内容摘自 《重学安卓:是让人 过目难忘 的 Android GUI 族谱解析啊!》
1.2 如何管理错综复杂的View?
通过自定义View可以绘制出我们任意想要的效果,一切看似很美好。正当你盯着屏幕欣赏自己的作品时,“啪”糊上来一个其他界面,一痛分析得知,原来其他app
也通过View
操控了屏幕,你也不甘示弱通过相同操作重新竞争到屏幕,如此反复进行 不可开交时屏幕黑了,得,还是换回塞班系统吧~~~
玩笑归玩笑,回归到问题本身。由于对View
的管理不当造成了屏幕很混乱的情况。按常理来讲当用户在操作一个app时肯定不希望其他app蹦出来,所以在此背景下需要一套机制
来管理错综复杂的View
。于是Android在系统进程中创建了一个系统服务WindowManagerService(WMS)
专门用来管理屏幕上的窗口
,而View
只能显示在对应的窗口上,如果不符合规定就不开辟窗口
进而对应的View
也无法显示
为什么WMS需要运行在系统进程?
由于每个
app
都对应一个进程,想要管理所有的应用进程,WMS
需要找一个合适的地方能凌驾于所有应用进程之上,系统进程是最合适的选择
1.3 不可缺少的窗口生命周期
自定义View
可以定制各种视图效果,窗口
可以让View
有条不紊的显示,一切又美好了起来。但问题又来了,每个App
都会有很多个界面(窗口)
,仅靠窗口/View
来控制窗口和视图会面临如下问题:
- 初始化时机不明确
- 无法感知
前景/背景
切换 - 不能及时销毁
- 等等...
以上一系列问题都是因为窗口
没有一套完善的生命周期导致的,如果将生命周期
强行加到窗口
上便违背了单一设计原则
。于是Android基于模板设计模式
设计出了Activity
并基于迪米特法则
将窗口
的管理屏蔽在内部,并暴露出对应的模版方法(onCreate、onStart、onResume...),让开发者只专注于视图排版(View)
和生命周期
,无需关心窗口
的存在
所以,单纯说通过Activity
创建一个界面似乎又不那么准确,一切窗口
均源自于WMS
,而窗口
中内容由View
进行填充,Activity
只是在内部"间接"
通过WMS
管理窗口并协调好窗口
与View
的关系,最后再赋予生命周期
等 功能而已。
关于Activity
如何管理窗口/View
? 请看第二小节
2. 实现流程
读源码的目的是为了理清设计流程,千万不要因果倒置陷入到代码细节当中,所以要懂得挑重点,讲究点到为止。本文为了提供更好的阅读体验,会将源码中大部分无用信息删掉,只保留精华。
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()
中执行stContentView()
才能显示的XML Layout
。关于stContentView()
顾名思义就是设置我们的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 mViews = new ArrayList();
/**
* 用来存储所有的ViewRootImpl
*/
private final ArrayList mRoots = new ArrayList();
/**
* 用来存储所有的LayoutParams
*/
private final ArrayList mParams =
new ArrayList();
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
可以获得焦点,对此我感到疑惑。通过源码的分析可知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
内部,开发者只需基于模板方法开发即可
参考文献
《Android全面解析之Window机制》
《重学安卓:是让人 过目难忘 的 Android GUI 族谱解析啊!》