Android Activity/View/Window/Dialog/Fragment 深层次关联(白话解析)

前言

很早就想就这几个UI 组件关系梳理一篇博客,但由于之前一些基础博客没梳理好,因此耽搁了。这些UI 组件不论对于初学者还是有一定开发经验的同学来说都是经常用到的,但是可能没有深究其中差异,而网上也没有统一梳理这方面知识的文章。
本篇文章尝试用简单的语言精确描述个中关联与差异,通过本篇文章你将了解到:

1、Window 与 Window.java/PhoneWindow.java 有啥关系?
2、Window 与 View 是如何关联上的?
3、View 与 ViewGroup 父与子嵌套交错?
4、Activity 与 Window、View 如何牵线搭桥?
5、Activity 与 Dialog/PopupWindow/Toast 该怎么选?
6、Activity 与Fragment 的联系与区别。
7、一个串起来的小故事

1、Window 与 Window.java/PhoneWindow 有啥关系?

系统里的Window

从最简单的Android Demo 开始:编写一个显示 "Hello World" 的App。

1、定义一个MainActivity。
2、MainActivity 指定加载(setContentView(xx))布局文件。

编写完成并运行到手机上,当点击桌面上该Demo 的图标后将会显示"Hello World"字符串,可以看出仅仅只需要简单的几步就可以在手机上显示一段文字,对于开发者来说是很简单,而简单的原因是开发者不需要关注底层显示问题,系统已经帮忙我们搞定了这一切。

每一个Activity 启动时都会向系统申请创建一个Window 用来展示界面,我们的App是运行在独立的进程,这此处的"系统"指的是系统进程:system_server。
App 进程通过Binder(Android 跨进程通信方式) 告诉系统服务:WMS(WindowManagerService),请为我创建一个Window(窗口)用来显示我的UI。


image.png

WMS 收到请求后,将会创建WindowState 对象,该对象用来描述Window 的一切属性,也是WMS 里表示"窗口"的实体。

应用里的Window

在App 进程也会涉及到Window,只不过这并不是真正意义上的"窗口",它叫:Window.java,可以看出这是个抽象类,它的唯一实现子类就是咱们熟知的PhoneWindow.java。
Window.java/PhoneWindow.java 作用:

1、将Activity 部分逻辑提取放在Window.java实现。
2、比如设置状态栏、导航栏、标题、主题等。
3、处理按键事件分发。

用到Window.java 的组件常见的有Activity与Dialog,它俩都使用DecorView 作为整个ViewTree的根,而PhoneWindow.java 持有DecorView实例,也就是说Activity与Dialog 对DecorView的部分操作放在PhoneWindow.java里完成了。
网上大部分文章通常举例说:

Activity 包含Window,Window 包含View。

从方便理解层次关系的角度来看上面这句话没问题,因为从代码角度出发:Activity 持有PhoneWindow.java 实例,PhoneWindow.java 又持有RootView(DecorView),这么看起来就是包含关系。不过你真要较真的话:
Activity 并不是窗口,Window.java 也没表示窗口,它俩就不存在所谓的窗口包含关系。
而Window.java/PhoneWindow.java 与系统里的Window 没有什么直接联系,两者不是一个概念。

后续提及的Window没有特殊说明指的是系统里的Window。

2、Window 与 View 是如何关联上的?

Window 提供给应用进程的对象

既然说了Window.java与系统里的Window不是同一个概念,那么在Activity里的布局文件如何展示到系统里的Window上的呢?
当App 进程请求系统创建Window时,调用栈如下:

WindowManager.addView(xx)-->WindowManagerImpl.addView(xx)-->WindowManagerGlobal.addView(xx)-->ViewRootImpl.setView(xx)
-->Session.addToDisplay(xx)-->WindowManagerService.addWindow(xx)

其中Session.addToDisplay(xx) 及其之后的方法是在系统进程里执行的,addWindow(xx)里会构造WindowState。
以上流程调用结束,系统里的Window 就创建完毕了。
现在需要将该Window与应用进程关联起来。

我们知道Android 是事件驱动的,当提交界面刷新动作后,这些动作将会被缓存,等待屏幕刷新信号到来时才会真正执行这些刷新动作,而此处的执行入口即为:ViewRootImpl.performTraversals()。
该方法里调用了:ViewRootImpl.relayoutWindow(xx),进而调用了,Session.relayout(xx):

#ViewRootImpl.java
       int relayoutResult = mWindowSession.relayout(mWindow, mSeq, params,
                (int) (mView.getMeasuredWidth() * appScale + 0.5f),
                (int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility,
                insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
                mTmpFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
                mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout,
                mPendingMergedConfiguration, mSurfaceControl, mTempInsets);
        if (mSurfaceControl.isValid()) {
            //mSurfaceControl 已经有效,填充mSurface
            mSurface.copyFrom(mSurfaceControl);
        } else {
            destroySurface();
        }

mSurface 是ViewRootImpl.java 里的成员变量,定义如下:

    @UnsupportedAppUsage
    public final Surface mSurface = new Surface();

总结来说:

在执行relayout(xx)之后,WMS 端的surface 实例存放在SurfaceControl里,然后再赋值给应用进程里的mSurface变量。
此时应用进程间接拥有了Window 的Surface。

image.png

此处涉及到Binder通信,更详细的Binder请移步:Binder"秒懂"系列

应用进程绘制到Window上

应用进程拿到了WMS的Surface,接下来就需要将界面绘制到该Surface上,问题的重点是如何绘制到Surface上。
Android 绘制分为软件绘制与硬件绘制,不论是哪种绘制方式最终都要通过Surface,以软件绘制为例:

1、通过Surface.lockCanvas(xx)拿到Canvas对象。
2、通过Canvas绘制任意的界面。

最开始的"Hello World"是个字符串,因此我们可以用TextView来展示它,而TextView本质是通过Canvas.drawText("Hello World")来绘制的。
Surface可以理解为一个展示的面,而Canvas则是画布(绘制各种图形的API集合)。

image.png

通过画布的各种操作,最终效果呈现在Surface上。

至此我们知道Window和View的关联过程:

1、应用进程请求WMS 创建Window。
2、应用进程拿到WMS Surface。
3、应用进程通过Surface拿到Canvas。
4、应用进程通过将Canvas传递给ViewTree的根(RootView)。
5、ViewTree将Canvas一层层传递给各个ViewGroup/View。
6、ViewGroup/View 在onDraw(Canvas)里拿到Canvas进行绘制。
7、最终效果将呈现在Surface上,也就是说Window 有了内容。

明显地可以看出,以上过程与Window.java/PhoneWindow.java/Activity 并无关系,我们可以脱离三者将任意想要显示内容显示在Window上。
显示任意Window 攻略可移步:Window/WindowManager 不可不知之事

3、View 与 ViewGroup 父与子嵌套交错?

为什么需要View

当需要往Window上展示界面时,我们除了需要准备绘制的内容,如文本、图片。还需要知道绘制在Window的哪个位置,绘制的内容展示的尺寸有多大。
这些在Canvas里都有对应的方法:

绘制文本、图片
Canvas.drawText(xx)、Canvas.drawBitmap(xx)。

绘制的位置
Canvas.translate(xx)

绘制的尺寸
Canvas.clipRect(xx)

虽然都是可以通过Canvas控制,但是若是界面元素很多的话,那么重复的工作就比较多,尤其是绘制的位置与尺寸这俩步骤显然可以抽出作为公共的步骤。
而View 的作用之一就是封装了以上三个步骤,就是View里典型的三大步骤:

测量、摆放、绘制。

当我们需要在Window上展示不同的界面元素时,只需要定义不同的View对象即可,这样就方便了许多。
系统提供的文字View(TextView),图片View(ImageView)等即是View的具象化。

View 三大过程请移步:View测量/摆放/绘制 终于懂了

为什么需要ViewGroup

再考虑一个场景:想要在Window里展示3个界面元素,并且是纵向排布的。


image.png

这种场景的使用范围很广,没必要每次都重新设置View 之间的排列位置,于是将线性纵向排列拎出来作为一个公共组件,此时就引入了ViewGroup。
ViewGroup 描述了一组View的展示规则,系统提供的LinearLayout、FrameLayout等就是ViewGroup的具象化。

ViewGroup 顾名思义就是个组,其内部可以存放View也可以存放ViewGroup,通过嵌套包含,构成了ViewTree,最终展示在Window上。


image.png

4、Activity 与 Window、View 如何牵线搭桥?

Activity 的引入

Window与View 是通过Surface/Canvas 关联上的,换句话说想要在Window上展示界面元素,实际上只需要调用一个方法:

    //textView 表示待展示的View
    //layoutParams 表示对textView 布局的约束
    windowManager.addView(textView, layoutParams)

windowManager 实例通过获取系统服务而得到:

        //获取WindowManager实例,这里的App是继承自Application
    WindowManager wm = (WindowManager) App.getApplication().getSystemService(Context.WINDOW_SERVICE);

添加构建单个Window很简单,试想一下若是添加多个Window,那么这些Window之间的关系是如何处理呢?比如Window2显示后需要将Window1隐藏,点击事件该流转到哪个Window上,状态栏、导航栏的设置,页面的生命周期是如何确定的等问题,单靠Window显得力不从心。
Activity 作为一个独立页面的承载者,拥有完整的生命周期,当需要展示一个页面时,我们仅仅只需要将待展示的页面布局(View)关联到Activity,最终Window展示的结果即是我们的布局文件。
因此问题的重点是:
1、Activity 与View 如何关联?
2、Activity 与Window 如何关联?

Activity 与 View的关联

通常编写一个Activity时,需要重写其onCreate(xx)方法,在该方法里调用:

setContentView(R.layout.activity_main)

注:调用该方法之前PhoneWindow实例已经创建。
Activity 关联了布局文件:R.layout.activity_main,我们统称为View。
setContentView(xx) 主要功能如下:

1、创建DecorView,作为整个ViewTree的RootView,DecorView下还挂了其它的View/ViewGroup,有个ViewGroup叫做"content"。
2、PhoneWindow持有该DecorView。
3、将R.layout.activity_main 布局文件加载到内存,实例化为ViewGroup/View。
4、将实例化后的布局文件加入到DecorView里子布局"content"里。
5、此时一个完整的ViewTree建立完毕。

至此,Activity 已经关联了PhoneWindow,通过PhoneWindow间接持有了DecorView。

Activity 到View 的流转更详细请移步:Android Activity创建到View的显示过程

Activity 与 Window的关联

Activity 执行完成onCreate(xx)后,经过"Start"阶段到达"Resume"阶段,此时界面需要真正展示了。
在"Resume"阶段会调用:

ActivityThread.handleResumeActivity(xx)

该方法里有个重要的操作:

1、找到当前的Activity 实例,进而找到所持有的PhoneWindow实例。
2、通过PhoneWindow实例,找到关联的ViewTree根View:DecorView。
3、通过WindowManager.addView(DecorView,layoutparam)将DecorView 添加到Widnow里。

可以看出,Activity 内部实现了一些基础页面配置(DecorView 里处理了状态栏、导航栏、一些主题设置等),实现了ViewTree的构建(自定义的布局挂到DecorView某个子布局里),实现了将整个ViewTree添加到Window的操作。
大部分场景下,我们仅仅只需要关注布局文件(R.layout.activity_main)的编写与逻辑处理即可,剩下的工作交给Activity处理,最终我们想要展示的效果将会在Window里呈现。

更详细的DecorView分析请移步:Android DecorView 一窥全貌(上)

5、Activity 与 Dialog/PopupWindow/Toast 该怎么选?

有时候我们并不需要一个完整的页面,仅仅需要一个弹框提示即可,这个时候会考虑使用Dialog/PopupWindow/Toast 等组件。
请记住一个点:不管是什么样的UI 组件,最终都需要通过WindowManager.add(xx)添加到Window里。

Dialog/PopupWindow/Toast 最终都是通过WindowManager.add(xx)加载的,只不过是它们设定的Window尺寸没有占满整个屏幕,而是由外部设定的Window尺寸。
它们没有生命周期,其中Dialog/PopupWindow 依赖于Activity 上下文(Context,Token限制)。

当不需要占满屏幕、UI 简单、逻辑简单、偏重于提示/选择之类的场景时,Dialog/PopupWindow/Toast 是比较好的选择。
至于它们内部的区别,请移步:Dialog/PopupWindow/Toast 到底该怎么选

6、Activity 与Fragment 的联系与区别。

有时我们想要在不同的case下显示不同的页面,例如页面顶部有几个Tab栏,新闻、娱乐、学习等板块,点击不同的tab展示不同的页面。用Activity 作为页面承载的话有点大材小用,并且过渡没那么流畅。用Dialog的话因为没有生命周期管控,一些逻辑没法闭环(比如后台的网络请求,数据库加载等)。
此时就需要考虑使用Fragment。
Fragment 有如下特点:

1、跟随Activity 拥有生命周期。
2、将View 封装并拥有独立的处理逻辑。
3、Fragment 构建、切换速度比Activity 快。

但也有缺点:

1、生命周期复杂。
2、必须依赖于Activity。

更多Fragment 解析请移步:Android Fragment 要你何用?

7、一个串起来的小故事

1、WMS 能够构建并展示窗口,它作为一个公共的服务,需要提供给其它App(应用)创建并填充窗口,于是他提供了Surface给其它App使用。
2、应用拿到Surface并从中取出Canvas后就可以绘制任意的图形了。
3、Canvas 绘制需要设定绘制的起点,绘制的尺寸以及绘制的内容,这些都是必经之路,每次展示都需要设定这些参数有点冗余,于是View出现了。
4、View 封装了测量、布局、绘制 等操作,应用开发者只需要关注如何绘制即可,甚至绘制都无需过多关心,比如TextView、ImageView 都是系统封装好了的,仅仅需要关注具体的内容即可。
5、有一些布局的排列比较常见且规律,比如线性垂直排列View,这个时候就引入了ViewGroup,有了它各个View的顺序、位置编排都能实现,甚至我们都不需要关心这些,比如LinearLayout、FrameLayout 都是系统封装好了的排列规则。
6、此时通过View/ViewGroup 已经能够填充Window的内容了,但还是不够,因为还是要处理多个Window的交互,这个时候Activity 出现了。
7、Activity 能够设定默认的RootView(DecorView),我们仅仅只需要将布局文件关联到Activity就可以有一个统一的展示风格,比如统一的主题、标题栏等。
8、Activity 承载越来越多的工作量,为了减轻它的负担,PhoneWindow.java/Window.java 出现了,能够帮Activity 处理状态栏、导航栏、事件分发等一些操作。
9、Activity 还是太重了,一些场景并不适合,比如只想弹出一个提示框、选择框等,杀鸡焉用牛刀? 于是Dialog/PopupWindow/Toast 出现了。
10、而Dialog/PopupWindow/Toast 没有生命周期,用Activity 直接控制View又增加了Activity的工作量,于是Fragment 帮助Activity 管理了一些较为独立的View。

本文基于Android 10.0
若有疑问或是想要了解更细节的知识,请留言或私信我。

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android

1、Android各种Context的前世今生
2、Android DecorView 必知必会
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分发全套服务
6、Android invalidate/postInvalidate/requestLayout 彻底厘清
7、Android Window 如何确定大小/onMeasure()多次执行原因
8、Android事件驱动Handler-Message-Looper解析
9、Android 键盘一招搞定
10、Android 各种坐标彻底明了
11、Android Activity/Window/View 的background
12、Android Activity创建到View的显示过
13、Android IPC 系列
14、Android 存储系列
15、Java 并发系列不再疑惑
16、Java 线程池系列
17、Android Jetpack 实践与原理系列

推荐阅读更多精彩内容