一篇通俗易懂的Android视图系统设计与实现
Posted 涂程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一篇通俗易懂的Android视图系统设计与实现相关的知识,希望对你有一定的参考价值。
好文推荐
作者:Bezier
前言
说到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<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
内部,开发者只需基于模板方法即可开发
最后
小编在网上收集了一些 Android 开发相关的学习文档、面试题、Android 核心笔记等等文档,希望能帮助到大家学习提升,如有需要参考的可以直接去我 CodeChina地址:https://codechina.csdn.net/u012165769/Android-T3 访问查阅。
以上是关于一篇通俗易懂的Android视图系统设计与实现的主要内容,如果未能解决你的问题,请参考以下文章
Android UI设计与开发使用ViewPager实现欢迎引导页面
对Spring中的IOC与AOP简单理解(简单的理解,通俗易懂)