一篇通俗易懂的Android视图系统设计与实现_java

前言

说到​​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主线程
  • ActivityThreadmain方法首先会调用Looper.loop(),用来循环处理主线程Hanlder分发的消息。
  • 接下来的main方法会发送一个BIND_APPLICATION的消息,Looper收到后会通过Binder IPC通知AMS创建App进程对应的Application
  • Application创建后会再次通过Binder IPC通知AMS要创建ActivityAMS验证后会回到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()​​中执行​​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<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​​内部,开发者只需基于模板方法即可开发