一篇通俗易懂的 Android 视图系统设计与实现,android 开发框架搭建
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
给每个窗口
增加生命周期,让窗口
切换更加优雅
最后
今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如 Handler 机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。
最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司 20 年的面试题,把技术点整理成了视频和 PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。
还有?高级架构技术进阶脑图、Android 开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
**【Android 核心高级技术 PDF 文档,BAT 大
《Android 学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
厂面试真题解析】**

【算法合集】

【延伸 Android 必备知识点】

评论