写点什么

一篇通俗易懂的 Android 视图系统设计与实现,android 开发框架搭建

作者:嘟嘟侠客
  • 2021 年 11 月 28 日
  • 本文字数:5100 字

    阅读完需:约 17 分钟

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()执行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<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的单例对象统一管理,最后执行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给每个窗口增加生命周期,让窗口切换更加优雅

最后

今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如 Handler 机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。


最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司 20 年的面试题,把技术点整理成了视频和 PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。


还有?高级架构技术进阶脑图、Android 开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。


**【Android 核心高级技术 PDF 文档,BAT 大


《Android 学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享


厂面试真题解析】**



【算法合集】



【延伸 Android 必备知识点】



本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

用户头像

嘟嘟侠客

关注

还未添加个人签名 2021.03.19 加入

还未添加个人简介

评论

发布
暂无评论
一篇通俗易懂的Android视图系统设计与实现,android开发框架搭建