原文链接 了解安卓的视图体系结构

当咱们想要写一个页面的时候,通过一个Activity,而后调用其setContentView办法,把一个布局文件当作一个参数传递过来,而后一个页面就好了,然而除此之外,咱们还须要与一些组件打交道,比方像Window,WindowManager,那么这些货色到底 与咱们的页面布局有什么关系,明天就来学习一下,以便对整体窗口有个更分明的认知。

布局是一颗View tree

先从一个最简略的例子登程,平时咱们写一个页面,都从一个布局文件登程。这其实是在构建一个View tree,为啥肯定是tree呢,因为咱们的布局文件,无论有如许的简单,都是从一个根(通常是一个ViewGroup对象)开始的,父布局外面再写子布局,比方这样的:

<LinearLayout id="app_root">  <TextView id="label"/>  <Button id="submit"/></LinearLayout>

这会造成一个树状构造:

| app_root<br/>
   |- label<br/>
   |- submit<br/>
作为一个开发者,写布局是咱们再相熟不过的了,次要就是用所相熟的各种Layout和View一起来构建想要的页面。

所写的布局,最终会生成一颗View tree,是一个树状的数据结构,每一个节点都是一个View对象(ViewGroup和View)。因而,布局优化的一个是感觉重要的点就是要先缩小View tree的深度(也即平时所说的缩小布局的嵌套),再想方法缩小广度(缩小个数)。

那么,咱们写的布局的父布局又是哪里呢?这就又波及两个货色,一个叫做decorView和contentView的货色。

DecorView与ContentView

咱们平时所见的屏幕窗口的根布局是一个叫做DecorView的货色,它是咱们通常意义上整个屏幕的根节点,它蕴含了下面的Status bar和下方的Navigation bar,以及属于应用程序的两头局部。它的源码门路是frameworks/base/core/java/com/android/internal/policy/DecorView.java。它是一个实在的view,它是FrameLayout的子类。

它上面有一个id为android.R.id.content的FrameLayout,咱们平时在Activity中调用setContent时所传过来的布局文件所生成的View tree都是增加在这个FrameLayout上面,所以,通常对于咱们一个Activity来说,这个FrameLayout是间接意义上的根节点,咱们所写的布局都是增加它上面的。

ContentView所引申进去的奇技淫巧

布局优化技巧

首先,一个是布局的优化技巧,能够缩小View tree的层级:如果你写的布局中根节点也是一个FrameLayout,那么能够间接用merge节点,把子view全副都间接加挂到后面提到的零碎创立的Activity的根布局下面。

<merge>  <Text />  <Button /></merge>

这能够把View tree缩小一个层级(深度减1)。

页面内即插即用的弹窗

每个Activity都被回挂在一个id是android.R.id.content的FrameLayout上面,利用这一点,能够做一些即插即用的弹窗,即插即用的意思是,不必写在布局外面,而且显示的工夫是不固定的,可能很多时候都不显示,在某个特定的逻辑或者工夫才显示。就好比某些电商特定节日的弹窗一样,这种货色,一年也显示不了几回,如果间接增加在布局外面(哪怕你用ViewStub),不够优雅,毕竟不是惯例逻辑下会呈现的页面,这时能够利用content来做一些即时弹窗:

FrameLayout container = activity.findViewById(android.R.id.content);View pop = <create or inflate your own view>;container.addView(pop);

只有你能取得到Activity的实例(这个并不难),那么就能够十分优雅的增加弹窗,逻辑代码和布局文件都会相当独立,甚至能够用插件模式来异步加载。再进一步,如果 增加一个WebView,那么就能够做得更加的前端化,实时化和定制化,好多电商的弹窗就是这么干的。

Window与WindowManager

作为利用开发者,咱们看一个View tree其实就是一坨布局,这是站在一个十分小的角度去看的,但如果站在整体零碎架构角度来看的话,就会发现应用程序所在的view tree仅是零碎可视化窗口架构中的末端,View只是用来构建视图的根本砖块而已。对于整体View tree是如何渲染的,何时渲染,这就波及到了整体零碎架构层面的重量级组件了。

对于现化代的视图窗口架构(Modern GUI),都有一个window server,作来治理视图窗口的外围组件,比方X11,Android当中也不例外。在Android外面,WindowManager就是专门用于治理视图窗口的,它是零碎级别的server叫window manager server是一个零碎级别的常驻过程,由init.rc启动。而Window则是一个根本的窗口的逻辑上的形象。对于Window以及WindowManager自身就是相当大的话题,都能够独自写本书,这时不做过多的探讨,对于咱们利用开发者来说,理解一下根本的常识就够用了。

每一个Activity,都有一个Window对象,所有所有与GUI无关的事件,都委派给了Window对象,Actvity自身并不参加GUI的具体流程,比方像下面提到的DecorView,ContentView等View tree的构建与治理,View tree的渲染,以及像事件的解决,都是Window对象解决的。Window是WindowManager的根本对象,与其server之间通过IPC通信,Window是供应用程序端应用的,其实真正所有都把握在window server手中。Activity和Dialog应用的对象都是PhoneWindow,它在frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java,Window对象会具体负责创立像DecorView之类的一些基础设施。最为要害的一个办法就是其PhoneWindow#installDecor()办法,这个办法外面会先调用generateDecor()创立mDecor,它就是后面讲到的DecorView对象,再通过generateLayout()创立mContentParent对象,它就是后面讲到的id是android.R.id.content的那个FrameLayout,Activity或者Dialog通过setContentView送过来的View tree就是加在它的上面的。

WindowManager是一个接口(Android零碎的代码接口用的特地多,很多要害的架构层面的组件 都是接口,理论应用的都是其一个实现。)理论应用的是WindowManagerImpl对象,而它也没干啥,它把事件 又委派给另外一个叫做WindowManagerGlobal的对象,这个WindowManagerGlobal则是GUI端的最初一站,它负责与wms(WindowManagerServer)通信。它在frameworks/base/core/java/android/view/WindowManagerGlobal.java

须要留神WindowManagerGlobal是一个单例,也就是说每一个应用程序(严格来说是每一个过程只有一个实例,但安卓下面带有GUI的应用程序只能存活在一个过程,所以能够了解 为一个应用程序)只有一个实例,所以它治理着一个应用程序中的所有的View tree。从它的成员中便可看出,它有一坨ViewRootImpl对象(一个列表),而每一个ViewRoot对象治理着一颗View tree。

最为要害的一个办法就是WindowManagerGlobal#addView,每一个Window的持有者对象(如Activity或者Dialog)都是通过这个办法将其DecorView对象增加给WindowManager的。addView办法,会先创立一个ViewRootImpl对象,而后把要增加的view以及刚创立进去的ViewRootImpl都放进它的列表中,最初再调用ViewRootImpl#setView(view),这就把几大要害对象建设好了连贯,接下来的事件就归ViewRootImpl了。这里还有一个相当要害的对象,那就是LayoutParams,WindowManagerGlobal也有一个列表外面存着每个Viewtree根节点(也就是Decor view)的LayoutParams。

ViewRootImpl又是个啥

Window是从手机零碎角度来对待的窗口的概念,而View tree则是从应用程序角度构建GUI页面的概念,view tree是Window的一部分,Window对象持有mView,而这个mView就是下面提到的DecorView,也即是View tree的根节点。这里又要波及另外一个对象ViewRootImpl,它并非是View tree的一部分,尽管名字上比拟容易混同,因为它并不是View的子类,所以它不是任何一个View tree的节点,它的职责是治理View tree,像渲染以及事件派发,都是Window间接通过ViewRootImpl来进行的。在代码中理论应用的是ViewRootImpl对象,它实现了ViewParent接口。

所以,ViewRootImpl对象是值得细细钻研的,因为实际上是它在治理着GUI零碎--view tree的治理,渲染的三大步(measure, layout和draw)以及事件的派发,最源头的逻辑都在这个对象外面,当然 它也是非常复杂的,源码大略有1万行左右。

ViewParent又是个啥

它是一个接口,行使的职责是治理子View,也就是说在View tree当中治理子View的行为的汇合便是ViewParent接口。View tree的节点都是View的子类,所以,你看ViewRootImpl实现了ViewParent接口,它是负责管理Window外面的View tree的。另外一个就是ViewGroup,ViewGroup是View的子类,所以它是Viewtree的一部分,父节点都是ViewGroup,它外围就两样货色一个是子View的列表,另外就是ViewGroup也实现了ViewParent的接口,因为它也要治理它的子节点(也即子View)。

Activity到底是个啥货色

它是零碎的四大外围组件之一,如果想构建GUI页面,则Activity是绕不开的。如果再具体一点,Activity是一个零碎给你的交融了利用生命周期治理,组件级别复用(Intent相干)和窗口治理的组件,生命周期也即ActivityManager干的事件,它通过Activity的回调通知你;而GUI则是通过Activity的Window对象帮你实现(Activity的布局和事件的解决都是委派给其持有的Window对象来解决)。

如果,把Activity的Window对象拿掉,那么它跟一个Service组件就基本上没有差异了。如果把Activity的Intent相干拿掉,那么它跟一个Dialog就没啥区别了。

Fragment又是个啥

坦白说,Fragment是Google挖的一个大坑,这玩意儿不合乎Android的外围设计思维,因为Android入世的时候并没有它,是起初Google跟水果平台抄来的一个不三不四的货色,后果全是坑。在它刚进去的一些年,Google竭力的举荐应用Fragment,然而近一两年,又不举荐了。

Fragment实质上就是一个强加了生命周期函数回调的View,因为显示Fragmeng时,都是把它替换一个View或者增加到一个ViewGroup下面,所以它就是一个View,或者说一个View tree中的节点。然而强加了生命周期的回调。光是这两点,其实也没有啥,毕竟生命周期对于View是重要的,个别时候咱们要在onResume与onPause之间才让View处于active状态。

Fragment最大的问题在于它的异步机制和状态复原机制,也就是说用FragmentManager#commit了当前,具体啥时候Fragment会真正显示进去,咱们是无法控制的,这是相当的坑;它的状态复原机制就更加的坑,状态复原这个货色如果全让程序员来负责也还好,就像Activity的设计一样,然而如果框架帮你做了一些事件,但又不残缺,这就坑了,对于状态复原的坑能够参考这篇文章来具体的理解。

DialogFragment

这个实质上是Dialog,然而被包了一层Fragment,所以它会有Fragment的个性,然而Window和View tree则是属于Dialog的。

留神:FragmentTransaction#add(Fragment fragment, String tag)有一个办法是不须要提供父布局,这是为没有惯例布局筹备的,因为无奈把布局增加到Activity的现有View tree之中。个别状况下,咱们是不会应用这个办法的,目前看仅在DialogFramgment中应用这个办法,那是因为Dialog自身有Window和view tree。

不在Activity view tree外面的窗口控件

一般来讲,咱们想要显示的页面都会放进布局外面,也就是说大部分时候咱们的页面都由Activity的view tree来实现。然而有些非凡的场景,却不是在view tree外面,比方弹窗,像Dialog,PopupWindow以及Toast,这些货色个别是用于弹出式的页面,由特定的逻辑触发,它与惯例页面最为显著的区别就是,它们与Activity的Window和View tree是独立开来的,它们并不是增加在以后Activity的view tree下面的。它们本人有独立的view tree,或者换句话说,它们是独立的Window。

咱们这里重点探讨它们与Window和以后Activity之间的关系,至于它们的根本应用办法,能够参阅其余文章。

Dialog

这里不说根本应用办法。

通过查阅源码,能够发现Dialog与Activity的实现相当相似,它外部也有一个独立的Window,也是通过WindowManager#addView把其ContentView(咱们提供的布局)加到屏幕下来的。因而,它与Activity也是互相独立的,是两个Window,两棵View tree。Dialog类外面还有getActionBar,OptionsMenu等相干的办法,但仿佛在理论应用当中比拟少用到。

Dialog最为外围的两个办法一个是其构造方法,这其中会创立Window对象,另外一个就是#show,外面能够看到,它是通过WindowManager#addView()办法,来把它的mDecorView增加到窗口体系当中的,这与Activity其实是一样的。

为啥显示Dialog肯定须要Activity,个别Context却不能够

应用过Dialog的人都晓得,创立Dialog时肯定要传递Activity为其参数,只管构造方法外面申明的是Context。后面提到,Dialog有本人的Window和View tree,实践上它跟Activity是没有关系的。

如果,用一个非Activity作为Context传给Dialog,报错,是WindowManager抛出来的异样,说:

android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?        at android.view.ViewRootImpl.setView(ViewRootImpl.java:1093)        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:409)        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:110)        at android.app.Dialog.show(Dialog.java:342)

而Dialog#setOwnerActivity(Activity)办法在创立Dialog之后再把相干Activity塞过来,也是不行的,必须传入的Context参数要是一个Activity实例才能够。

最后认为,能够从它的构造方法中看出为啥肯定须要Activity,就是因为须要theme.但其实并不是,因为theme是能够通过resource id传进去的。

关键点仍在于ViewRootImpl对象,因为这个异样是ViewRootImpl在其setView办法中抛出的,后面讲过,向一个Window增加布局最终会走到WindowManagerGlobal#addView,而它又是通过ViewRootImpl#setView来做具体事件 的,这个办法外面,会先获取以后的WindowSession,而后再把以后的Viewtree转化为窗口对象,增加给wms。所以最外围的中央还得看WindowManagerService#addWindow()这个办法,这个办法也相当之简单,然而还是能大略看懂它的意思。

艰深的来了解这是安卓零碎自身加的限度,也就是说窗口自身也是有逻辑关系的,能够简略了解 为树状关系,一个Activity是主Window,而由此Activity衍生进去的属于此Window的子Window,因而在增加子Window的时候,必须 要晓得它从属于哪个父Window,因而,你必须 传Activity实例给Dialog的构造方法,因为只有Activity才是有主Window的。然而这个具体的逻辑连贯却比拟奇怪,从下面的过程形容来看,WindowManager#addView到ViewRootImpl#setView,其实,都没有明确的把父Window,也就是Activity的Window传进来,那么WindowManagerService又是从哪里去找这个父Window呢?

WindowManagerService#addWindow办法,并没有传递父Window参数 进来,那就只能是它从传进来的参数取得的。这里一个很重要的货色就是token,它是一个IBinder对象,它是一个Window的标识,它存在Window的attris对象外面,这个就是WindowManager#LayoutParams对象,它的作用就是存储Window的特征参数,比方你要扭转Window的一些个性(艰深来说就是定制一下Window),那么通过扭转LayoutParams,就能够了。这个其实不难理解,咱们对View不就是通过其LayoutParams来扭转View的特征参数 么。都 是一样的。

Dialog对象在show()时会把其mDecor增加到WindowManagerService中去,其并未传父Window,只传了一个LayoutParams过来,其实玄机也就在LayoutParams之中,窗口的token,父token(标识着父窗口)以及像窗口的type都是在LayoutParams中。那么这个LayoutParams是哪里创立的呢?它是来自于Window对象的,而Dialog的mWindow成员实例是在结构时创立的,创立的是一个PhoneWindow对象,并且把结构Dialog传进来的Context对象传给了PhoneWindow的对象,LayoutParams对象则是通过mWindow.getAttributes()得来的。因而啊,能够判定,PhoneWindow在生成LayoutParams时,会从传给其结构的上下文对象mContext中获取一些信息,如窗口的类型或者父窗口信息,而只有Activity对象才有窗口信息,并且能够作为父窗口,而一般 的Context对象是没有窗口的,由此能够解答咱们的纳闷了。

也能够显示独立于任何Activity的Dialog

窗口是有很类型的,WindowManagerService为了方便管理,所以针对Activity及其从属于子窗口(Dialog和PopWindow)做了相似tree构造的逻辑上的整顿,所以一般 的Dialog必须要能找到其主窗口(或者叫父窗口)。

但其实,咱们常常能见到一些十分牛逼的Dialog,能够显示 在任何Activity之上,如电源没了,或者音量调节,等等。这些是叫作system dialog,须要非凡权限 能力显示进去的。治理来了解,零碎级别的组件 才有权限 显示system dialog。

其实,想一想也正当,作为一个应用程序,你在本人的生命周期内,显示内容给用户足够的信息就能够了。当用户来到了你的利用,你也没有必要再显示Dialog了。

:利用在后盾时,想在前台显示信息有其余的形式,如Notification等,这属于另外的话题,不做过多探讨。

能够弄个全屏的Dialog吗?

一般来讲呢,Actiivty都是全屏的,Dialog个别是非全屏的,能够把一个Activity弄成非全屏的,长的像Dialog一样,当成Dialog来应用,就在设置Activity的Theme时,用Theme.Dialog就能够了。

那么,反过来搞可不可呢,就是可不可以把惯例的Dialog弄成一个全屏的呢?

从Dialog的实现上来看,它有Window对象,甚至连Actionbar和OptionsMenu都有,所以从实现上来看,Dialog并不一定非要像咱们平时所应用的那样是一个对话框,它能做的事件 不比Activity少。默认Dialog的style就是一个平时的对话框,但其实,设置不同的style,就能够失去全屏的dialog。

     private void showFullscreenDialog() {        // Theme_Material_NoActionBar_Fullscreen is real full screen, i.e. hide the status bar.        Dialog dialog = new Dialog(this, android.R.style.Theme_Material_NoActionBar);        dialog.setContentView(R.layout.fullscreen_dialog);        dialog.findViewById(R.id.okay).setOnClickListener(view -> {            dialog.dismiss();        });        dialog.show();    }

:这里有点歧义,全屏意思是指铺满整个父Activity,严格意义上的全屏是要把状态栏也要暗藏掉。

PopupWindow

PopupWindow是一个独立的类,并不是View的子类,因而,它跟惯例的widget不一样,无奈间接增加到现有的View tree之中,这也导致它的实现形式比较复杂。

PopupWindow它并没有创立Window对象,然而它有一个相似于Window对象的DecorView的货色,它的根节点是一个叫做PopupDectorView的货色,其实是一个FrameLayout,咱们让PopupWindow显示的布局就是加在这个PopupDectorView上面。最重要的两个办法一个是preparePopup() 这个办法会创立根节点PopupDecorView,而后把咱们须要显示的mContentView以及还有一个PopupBackgroundView(也是一个FrameLayout,包裹在要显示的ContentView里面),放在PopupDecorView的上面,所以实在的构造是根节点是PoupDecorView,包了PopupBackgroundView,再包上要显示的mContentView,一共三层。

另外,一个办法就是invokePopup,外围逻辑是调用WindowManager#addView,把mDecorView增加到窗口零碎中以显示进去,前面的过程跟下面提到的Dialog的显示过程是一样的。那么PopupWindow又是如何找到Activity的主Window的呢?答案还是在LayoutParams中,办法preparePopup()的参数 是LayoutParams,如后面所述LayoutParams是最终会传递给WindowManagerService的,而这外面就蕴含了主窗口的信息。而这个LayoutParams对象是通过办法createPopupLayoutParams()得来的,而这个办法的参数 是一个IBinder对象,咱们晓得这个IBinder对象就标识着一个主窗口。那么PopupWindow的IBinder对象又从何而来呢?是通过View.getWindowToken()得来的,PopupWindow的显示 办法都要提供一个View如showAsDropDown,外面的参数是一个View,而这个View必须 是已显示的View tree中的一个节点,当初应该晓得一个窗口有一颗View tree,那么此View tree中的节点必定 晓得本人属于哪个窗口啊,由此便找到了主窗口。

另留神,PopupMenu,也是基于PopupWindow的,只不过弄成了Menu的样子(其实就是一个ListView)。

能够弄个全屏的PopupWindow吗?

当然 能够,只须要在结构PopupWindow时传入MATCH_PARENT作为其宽和高就能够了,不过这样做当前前面再抉择哪种show形式就不影响了,都是铺满Activity来显示。

     private void showFullscreenPopup() {        final View content = LayoutInflater.from(this).inflate(R.layout.fullscreen_dialog, null, false);        PopupWindow popup = new PopupWindow(content, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);        final View anchor = findViewById(R.id.fullscreen_popup);        // Key is the width and height passed to constructor, show does not affect anything.//        popup.showAtLocation(anchor, Gravity.NO_GRAVITY, 0, 0);        popup.showAsDropDown(anchor, 100, 200);        content.findViewById(R.id.okay).setOnClickListener(view -> {            popup.dismiss();        });    }

Toast又是个啥

这个大家都十分相熟了,每天都用到,用以给出一些十分弱的提醒。

它其实也是有独立Window的。Toast类自身比较简单,但它也是有一个专门的Server的叫NotificationManager,Toast也是一个客户端,间接做工作的是另一端的服务,这也是为何即便咱们的利用退到了后盾仍然能够show一个Toast。咱们用的最多的就是让其显示一段文字,但其实那只是它的一个十分根底的用法。从Toast的办法就可以看进去,它是能够承受一个View的,所以把一个布局的根节点传进去,那这个布局不就能够显示了么?

Toast能够显示简单布局吗?

尽管,通常咱们都是应用Toast.makeText办法,但这并意味着它只能显示纯文字,它是可发接管一个View作为其Content的,就通过其setView办法:

     private void showComplexToast() {        Toast toast = new Toast(this);        final View dialog = LayoutInflater.from(this).inflate(R.layout.fullscreen_dialog, null, false);        toast.setDuration(Toast.LENGTH_LONG);        toast.setView(dialog);        // This does not work, Toast cannot receive focus, i.e. it won't receive events from WMS        dialog.findViewById(R.id.okay).setOnClickListener(view -> {            toast.cancel();        });        toast.show();    }

不过呢,尽管Toast能够展现更为简单的布局,然而它是无奈接管用户事件,也就是说它是无奈解决点击事件的,你想有用户交互的话,是不能够的。

如此,如果你想显示一个相似Toast的,然而能够交互 的,那只能用PopupWindow或者Dialog来模仿,但这又只能是在利用在前台时显示;如果在后盾时,又想要有交互行为,那只能用Notification和PendingIntent了。

综合论断

说了这么多,心愿还没有看晕,总结一下:

  1. Window也是有构造 关系的,相似于View一样,像一样tree
  2. 每一个Window都有一颗View tree,DecorView是其根节点
  3. ViewRootImpl是用来治理View tree的
  4. Dialog和PopupWindow能够用以显示铺满Activity,甚至全屏的View
  5. Toast也能够展现简单布局

实战倡议

Activity应该只用于显示一个页面内的次要的,逻辑上都能够触达的布局,比方一上来用户就可见的所有货色,以及惯例操作能够触发的(如折叠开展等)。

Activity的View tree要尽可能的小,这样能力保障最好的渲染性能,其余的,很多一次性的,即插即用的,鲜有逻辑才会有触发的,这种布局,要尽可能的独立于Activity的View tree之外,以保障其布局和逻辑上的独立,也更不便保护,更能缩小Activity的view tree的体积。因为Dialog和PopupWindow也能够铺满整个Activity,所以,像一些用户疏导,新人疏导,经营流动,分享,等等一些惯例逻辑走不到的页面,都能够思考用Dialog和PopupWindow来实现。

原创不易,打赏点赞在看珍藏分享 总要有一个吧