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

return activity;

}

首先通过注释1处创建一个Activity对象,然后在注释2处执行其attach(..)方法,最后在通过callActivityOnCreate()执行ActivityonCreate()方法

先来看attach做了什么事情:

#Activity

final void attach(…){

mWindow = new PhoneWindow(this, window, activityConfigCallback);

mWindow.setWindowManager(…);

mWindowManager = mWindow.getWindowManager();

}

Activity会在attach()方法中创建一个PhoneWindow对象并复制给成员变量mWindow,随后执行WindowManagersetter、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可知 Activityattach()运行完毕后会执行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)创建mContentParentgenerateLayout(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处 会间接调用ActivityonResume方法

  • 注释2处 通过Activity获取PhoneWindow、DecorView、WindowManager,它们的创建时机前面小结有写,忘记的可以回翻阅读。

  • 注释3处 调用了WindowManageraddView方法,顾名思义就是将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());

}

内部调用了mGlobaladdView()方法,其实不光addView几乎所有WindowManager方法都是通过委托mGlobal去实现,这种写法看似很奇怪,但实际上这种设计不仅不奇怪而且还很精妙,具体精妙在何处?我列出以下三点:

  • WindowManager提供的功能全局通用不会与某个View/Window单独绑定,为了节省内存理应设计出一个单例

  • WindowManagerImp具备多个职责如Token管理、WindowManager功能等,所以通过单一设计原则WindowManager功能拆分到另一个类中即WindowManagerGlobal,并将其定义为单例。

  • 为了不违背迪米特法则又通过组合模式将WindowManagerGlobal屏蔽在内部。

回归正题,来看mGlobaladdView()方法:

#WindowManagerGlobal

/**

  • 用来存储所有的DecorView

*/

private final ArrayList mViews = new ArrayList();

/**

  • 用来存储所有的ViewRootImpl

*/

private final ArrayList mRoots = new ArrayList();

/**

  • 用来存储所有的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的单例对象统一管理,最后执行rootsetView()。 根据我多年阅读源码的经验 答案应该就在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方案,比如背景、标题、虚拟按键等

  • PhoneWindowWindow的唯一实现类,完善了Window的功能,并提供了事件的中转

  • WindowManager是一个接口,继承自ViewManager接口,提供了View的基本操作方法

  • WindowManagerImp实现了WindowManager接口,内部通过组合方式持有WindowManagerGlobal,用来操作View

  • WindowManagerGlobal是一个全局单例,内部可以通过ViewRootImplView添加至窗口

  • ViewRootImpl是所有ViewParent,用来管理View的绘制以及窗口的开辟

  • IWindowSessionIWindowSession类型的AIDL接口,可以通过Binder IPC通知WMS开辟窗口

至此关于Java Framework层面视图系统的设计与实现梳理完毕

综上所述


  • 一切视图均由Canvas而来

  • View的出现是为了提供视图模板,用来提升开发效率

  • 窗口可以让View有条不紊的显示

  • Activity给每个窗口增加生命周期,让窗口切换更加优雅

  • PhoneWindow只是提供些标准的UI方案,与窗口不等价

  • 可通过WindowManagerView添加到窗口

最后

为了方便有学习需要的朋友,我把资料都整理成了视频教程(实际上比预期多花了不少精力)

当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。

  • 无论你现在水平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,这四个字就是我的建议!!
  • 我希望每一个努力生活的IT工程师,都会得到自己想要的,因为我们很辛苦,我们应得的。

当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。

无论你现在水平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,没有人能随随便便成功。

加油,共勉。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值