《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统

时间:2024-01-07 11:45:50

《深入理解Android 卷III》即将公布,作者是张大伟。此书填补了深入理解Android Framework卷中的一个主要空白,即Android Framework中和UI相关的部分。

在一个特别讲究颜值的时代,本书分析了Android 4.2中WindowManagerService、ViewRoot、Input系统、StatusBar、Wallpaper等重要“颜值绘制/处理”模块

第6章 深入理解控件(ViewRoot)系统(节选)

本章主要内容:

·  介绍创建窗体的新的方法以及WindowManager的实现原理

·  探讨ViewRootImpl的工作方式

·  讨论控件树的測量、布局与绘制

·  讨论输入事件在控件树中的派发

·  介绍PhoneWindow的工作原理以及Activity窗体的创建方式

本章涉及的源代码文件名称及位置:

·  ContextImpl.java

frameworks/base/core/java/android/app/ContextImpl.java

·  WindowManagerImpl.java

frameworks/base/core/java/android/view/WindowManagerImpl.java

·  WindowManagerGlobal.java

frameworks/base/core/java/android/view/WindowManagerGlobal.java

·  ViewRootImpl.java

frameworks/base/core/java/android/view/ViewRootImpl.java

·  View.java

frameworks/base/core/java/android/view/View.java

·  ViewGroup.java

frameworks/base/core/java/android/view/ViewGroup.java

·  TabWidget.java

frameworks/base/core/java/android/widget/TabWidget.java

·  HardwareRenderer.java

frameworks/base/core/java/android/view/HardwareRenderer.java

·  FocusFinder.java

frameworks/base/core/java/android/view/FocusFinder.java

·  Activity.java

frameworks/base/core/java/android/app/Activity.java

·  PhoneWindow.java

frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java

·  Window.java

frameworks/base/core/java/android/view/Window.java

·  ActivityThread.java

frameworks/base/core/java/android/app/ActivityThread.java

6.1 初识Android的控件系统

第4章和第5章分别介绍了窗体的两个最核心的内容:显示与用户输入,同一时候也介绍了在Android中显示一个窗体并接受输入事件的最主要的方法。

可是这样的方法过于基本,不便于使用。直接使用Canvas绘制用户界面以及使用InputEventReceiver处理用户输入是一件非常繁琐恼人的工作,因为你不得不亲历亲为下面复杂的工作:

·  測量各个UI元素(一段文字、一个图片)的显示尺寸与位置。

·  对各个UI元素进行布局计算与绘制。

·  当显示内容须要发生变化时进行重绘。

出于效率考虑。你必须保证重绘区域尽可能地小。

·  分析InputEventReceiver所接收的事件的类型,并确定应该由哪个UI元素响应这个事件。

·  须要处理来自WMS的非常多与窗体状态相关的回调。

所幸Android的控件系统使得这些事情不须要我们亲历亲为。

自1983年苹果公司公布第一款搭载图形用户界面(GUI)操作系统的个人电脑Lisa以来的三十多年里,图形用户界面已经发展得相当成熟。不管是运行于桌面系统还是Web,每个面向图形用户界面的开发工具包(SDK)都至少内置实现了用户和开发人员所公认的一套UI元素,尽管名称可能有所差异。比如文本框、图片框、列表框、组合框、button、单选button、多选button,等等。Android的控件系统不仅延续了对各种标准UI元素的支持。还针对移动平台的操作特点添加了使用更加方便、种类更加丰富的一系列新型的UI元素。

注意 在Android中,一个UI元素被称为一个视图(View),然而,笔者觉得控件才是UI元素的更贴切的名字。因为UI元素不仅仅是为了向用户显示一些内容。更重要的是它们响应用户的输入并进行相应的工作。本书兴许部分将以控件来称呼UI元素(View)。

另外。本章的目的并非介绍怎样使用各种Android控件,而是介绍Android控件系统的工作原理。本章要求读者至少应了解使用Android控件的基本知识。

读者所熟知的Activity、各种对话框、弹出菜单、状态栏与导航栏等等都是基于这套控件系统实现的。因此控件系统将是继WMS与IMS两大系统服务之后的又一个须要我们攻克的目标。

6.1.1 还有一种创建窗体的方法

在这一小节里将介绍第二种创建窗体的方法,并以此为切入点来開始对Android控件系统的探讨。

这个样例将会在屏幕*显示一个button,它会浮在全部应用之上。直到用户点击它为止。市面上某些应用的悬浮窗就是如此实现的。

·  首先。读者使用Eclipse建立一个新的Androidproject,并新建一个Service。然后在这个Service中添加例如以下代码:

// 将button作为一个窗体加入到WMS中

private void installFloatingWindow() {

// ① 获取一个WindowManager实例

finalWindowManager wm =

(WindowManager)getSystemService(Context.WINDOW_SERVICE);

// ② 新建一个button控件

finalButton btn = new Button(this.getBaseContext());

btn.setText("Click me to dismiss!");

// ③ 生成一个WindowManager.LayoutParams,用以描写叙述窗体的类型与位置信息

LayoutParams lp = createLayoutParams();

// ④ 通过WindowManager.addView()方法将button作为一个窗体加入到系统中

wm.addView(btn, lp);

btn.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

// ⑤当用户点击button时,将button从系统中删除

wm.removeView(btn);

stopSelf();

}

});

}

privateLayoutParams createLayoutParams() {

LayoutParams lp = new WindowManager.LayoutParams();

lp.type = LayoutParams.TYPE_PHONE;

lp.gravity = Gravity.CENTER;

lp.width = LayoutParams.WRAP_CONTENT;

lp.height = LayoutParams.WRAP_CONTENT;

lp.flags = LayoutParams.FLAG_NOT_FOCUSABLE

| LayoutParams.FLAG_NOT_TOUCH_MODAL;

return lp;

}

·  然后在新建的Service的onStartCommand()函数中添加对installFloatingWindow()的调用。

·  在应用程序的主Activity的onCreate()函数中调用startService()以启动这个服务。

·  在应用程序的AndroidManifest.xml中添加对权限android.permission.SYSTEM_ALERT_WINDOW的使用声明。

当完毕这些工作之后,运行这个应用就可以得到如图6-1所看到的的效果。一个名为“Clickme to dismiss!”的button浮在其它应用之上。而点击这个button后,它便消失了。

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />

图 6 - 1浮动窗体样例的运行效果

读者能够将本例与第4章的样例SampleWindow做一个对照。

它们的实现效果是大同小异的。而然,本章的这个样例不管是从终于效果、代码量、API的复杂度或可读性上都有非常大的优势。这得益于对控件系统的使用。在这里,控件Button托管了窗体的绘制过程,而且将输入事件封装为了更具可读性的回调。而且加入窗体时所使用的WindowManager实例掩盖了client与WMS交互的复杂性。更重要的是,本例所使用的接口都来自公开的API,也就是说能够脱离Android源代码进行编译。这无疑会带来更方便的开发过程以及更好的程序兼容性。

因此,除非须要进行非常底层的窗体控制,使用本例所介绍的方法向系统中加入窗体是最优的选择。

6.1.2 控件系统的组成

从这个样例中能够看到在加入窗体过程中的两个关键组件:Button和WindowManager。Button是控件的一种。继承自View类。不仅仅Button,不论什么一个继承自View类的控件都能够作为一个窗体加入到系统中去。WindowManager事实上是一个继承自ViewManager的接口,它提供了加入/删除窗体,更新窗体布局的API,能够看作是WMS在client的代理类。

只是WindowManager的接口与WMS的接口相差非常大。差点儿已经无法通过WindowManager看到WMS的模样。这也说明了WindowManager为了精简WMS的接口做过大量的工作。

这部分内容也是本章的重点。

因此控件系统便能够分为继承自View类的一系列控件类与WindowManager两个部分。

6.2 深入理解WindowManager

WindowManager的主要功能是提供简单的API使得使用者能够方便地将一个控件作为一个窗体加入到系统中。本节将探讨它工作原理。

6.2.1 WindowManager的创建与体系结构

首先须要搞清晰WindowManager是什么。

准确的说。WindowManager是一个继承自ViewManager的接口。ViewManager定义了三个函数,分别用于加入/删除一个控件。以及更新控件的布局。

ViewManager接口的还有一个实现者是ViewGroup。它是容器类控件的基类,用于将一组控件容纳到自身的区域中,这一组控件被称为子控件。ViewGroup能够依据子控件的布局參数(LayoutParams)在其自身的区域中对子控件进行布局。

读者能够将WindowManager与ViewGroup进行一下类比:设想WindowManager是一个ViewGroup,其区域为整个屏幕。而当中的各个窗体就是一个一个的View。WindowManager通过WMS的帮助将这些View依照其布局參数(LayoutParams)将其显示到屏幕的特定位置。二者的核心工作是一样的。因此WindowManager与ViewGroup都继承自ViewManager。

接下来看一下WindowManager接口的实现者。本章最開始的样例通过Context.getSystemService(Context.WINDOW_SERVICE)的方式获取了一个WindowManager的实例,事实上现例如以下:

[ContextImpl.java-->ContextImpl.getSystemService()]

public Object getSystemService(String name) {

// 获取WINDOW_SERVICE所相应的ServiceFetcher

ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);

// 调用fetcher.getService()获取一个实例

returnfetcher == null ?

null : fetcher.getService(this);

}

Context的实现者ContextImpl在其静态构造函数中初始化了一系列的ServiceFetcher来响应getSystemService()的调用并创建相应的服务实例。看一下WINDOW_SERVICE所相应的ServiceFetcher的实现:

[ContextImpl.java-->ContextImpl.static()]

registerService(WINDOW_SERVICE, newServiceFetcher() {

public Object getService(ContextImpl ctx) {

// ① 获取Context中所保存的Display对象

Display display = ctx.mDisplay;

/* ② 倘若Context中没有保存不论什么Display对象。则通过DisplayManager获取系统

主屏幕所相应的Display对象 */

if (display == null) {

DisplayManager dm =

(DisplayManager)ctx.getOuterContext().getSystemService(

Context.DISPLAY_SERVICE);

display = dm.getDisplay(Display.DEFAULT_DISPLAY);

}

// ③ 使用Display对象作为构造函数创建一个WindowManagerImpl对象并返回

return new WindowManagerImpl(display);

}});

由此可见,通过Context.getSystemService()的方式获取的WindowManager事实上是WindowManagerImpl类的一个实例。

这个实例的构造依赖于一个Display对象。第4章介绍过DisplayContent的概念,它在WMS中表示一块的屏幕。而这里的Display对象与DisplayContent的意义是一样的,也用来表示一块屏幕。

再看一下WindowManagerImpl的构造函数:

[WindowManagerImpl.java-->WindowManagerImpl.WindowManagerImpl()]

publicWindowManagerImpl(Display display) {

this(display, null);

}

privateWindowManagerImpl(Display display, Window parentWindow) {

mDisplay = display;

mParentWindow = parentWindow;

}

其构造函数实在是出奇的简单,仅仅初始化了mDisplay与mParentWindow两个成员变量而已。从这两个成员变量的名字与类型来判断,它们将决定通过这个WindowManagerImpl实例所加入的窗体的归属。

说明 WindowManagerImpl的构造函数引入了一个Window类型的參数parentWindow。Window类是什么呢?以Activity为例,一个Activity显示在屏幕上时包括了标题栏、菜单button等控件,可是在setContentView()时并没有在layout中放置它们。

这是因为Window类预先为我们准备好了这一切,它们被称之为窗体装饰。除了产生窗体装饰之外,Window类还保存了窗体相关的一些重要信息。比如窗体ID(IWindow.asBinder()的返回值)以及窗体所属Activity的ID(即AppToken)。在6.6.1 介将会对这个类做具体的介绍。

或许在WindowManagerImpl的addView()函数的实现中能够找到很多其它的信息。

[WindowManagerImpl.java-->WindowManagerImpl.addView()]

publicvoid addView(View view, ViewGroup.LayoutParams params) {

mGlobal.addView(view, params, mDisplay, mParentWindow);

}

WindowManagerImpl.addView()将实际的操作托付给一个名为mGlobal的成员来完毕,它随着WindowManagerImpl的创建而被初始化:

privatefinal WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

可见mGlobal的类型是WindowManagerGlobal。而且WindowManagerGlobal是一个单例模式——即一个进程中最多仅有一个WindowManagerGlobal实例。全部WindowManagerImpl都是这个进程唯一的WindowManagerGlobal实例的代理。

此时便对WindowManager的结构体系有了一个清晰的认识,如图6-2所看到的。

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统

图 6 - 2 WindowManager的结构体系

·  ViewManager接口:WindowManager体系中最主要的接口。

WindowManager继承自这个接口说明了WindowManager与ViewGroup本质上的一致性。

·  WindowManager接口:WindowManager接口继承自ViewManager接口的同一时候。依据窗体的一些特殊性添加了两个新的接口。

getDefaultDisplay()用以得知这个WindowManager的实例会将窗体加入到哪个屏幕上去。

而removeViewImmediate()则要求WindowManager必须在这个调用返回之前完毕全部的销毁工作。

·  WindowManagerImpl类:WindowManager接口的实现者。它自身没有什么实际的逻辑,WindowManager所定义的接口都是交由WindowManagerGlobal完毕的。

可是它保存了两个重要的仅仅读成员。它们分别指明了通过这个WindowManagerImpl实例所管理的窗体将被显示在哪个屏幕上。以及将会作为哪个窗体的子窗体。因此在一个进程中,WindowManagerImpl的实例可能有多个。

·  WindowManagerGlobal类:它没有继承上述不论什么一个接口,但它是WindowManager的终于实现者。它维护了当前进程中全部已经加入到系统中的窗体的信息。另外,在一个进程中仅有一个WindowManagerGlobal的实例。

在理清了WindowManager的结构体系后,便能够探讨WindowManager是怎样完毕窗体管理的。其管理方式体如今其对ViewManager的三个接口的实现上。为了简洁起见,我们将直接分析WindowManagerGlobal中的实现。

6.2.2 通过WindowManagerGlobal加入窗体

參考WindowManagerGlobal.addView()的代码:

[WindowManagerGlobal.java-->WindowManagerGlobal.addView()]

publicvoid addView(View view, ViewGroup.LayoutParams params,

Display display, Window parentWindow){

......// 參数检查

final WindowManager.LayoutParams wparams =(WindowManager.LayoutParams)params;

/* ① 假设当前窗体须要被加入为还有一个窗体的附属窗体(子窗体),则须要让父窗体视自己的情况

对当前窗体的布局參数(LayoutParams)进行一些改动 */

if(parentWindow != null) {

parentWindow.adjustLayoutParamsForSubWindow(wparams);

}

ViewRootImpl root;

ViewpanelParentView = null;

synchronized (mLock) {

......

// WindowManager不同意同一个View被加入两次

int index = findViewLocked(view, false);

if (index >= 0) { throw new IllegalStateException("......");}

// ② 创建一个ViewRootImpl对象并保存在root变量中

root = new ViewRootImpl(view.getContext(), display);

view.setLayoutParams(wparams);

/* ③ 将作为窗体的控件、布局參数以及新建的ViewRootImpl以同样的索引值保存在三个

数组中。到这步为止,我们能够觉得完毕了窗体信息的加入工作 */

mViews[index] = view;

mRoots[index] = root;

mParams[index] = wparams;

}

try{

/* ④ 将作为窗体的控件设置给ViewRootImpl。

这个动作将导致ViewRootImpl向WMS

加入新的窗体、申请Surface以及托管控件在Surface上的重绘动作。这才是真正意义上

完毕了窗体的加入操作*/

root.setView(view, wparams, panelParentView);

}catch (RuntimeException e) { ...... }

}

加入窗体的代码并不复杂。当中的关键点有:

·  父窗体改动新窗体的布局參数。可能改动的仅仅有LayoutParams.token和LayoutParams.mTitle两个属性。mTitle属性不必赘述,仅用于调试。而token属性则值得一提。

回想一下第4章的内容,每个新窗体必须通过LayoutParams.token向WMS出示相应的令牌才干够。

在addView()函数中通过父窗体改动这个token属性的目的是为了减少开发人员的负担。开发人员不须要关心token究竟应该被设置为什么值。仅仅需将LayoutParams丢给一个WindowManager。剩下的事情就不用再关心了。

父窗体改动token属性的原则是:假设新窗体的类型为子窗体(其类型大于等于LayoutParams.FIRST_SUB_WINDOW并小于等于LayoutParams.LAST_SUB_WINDOW),则LayoutParams.token所持有的令牌为其父窗体的ID(也就是IWindow.asBinder()的返回值)。否则LayoutParams.token将被改动为父窗体所属的Activity的ID(也就是在第4章中所介绍的AppToken)。这对类型为TYPE_APPLICATION的新窗体来说非常重要。
从这点来说。当且仅当新窗的类型为子窗体时addView()的parentWindow參数才是真正意义上的父窗体。这类子窗体有上下文菜单、弹出式菜单以及游标等等,在WMS中,这些窗体相应的WindowState所保存的mAttachedWindow既是parentWindow所相应的WindowState。

然而另外还有一些窗体。如对话框窗体,类型为TYPE_APPLICATION。 并不属于子窗体,但须要AppToken作为其令牌,为此parentWindow将自己的AppToken赋予了新窗体的的LayoutParams.token中。此时parentWindow便并非严格意义上的父窗体了。

·  为新窗体创建一个ViewRootImpl对象。

顾名思义,ViewRootImpl实现了一个控件树的根。它负责与WMS进行直接的通讯。负责管理Surface。负责触发控件的測量与布局。负责触发控件的绘制,同一时候也是输入事件的中转站。总之。ViewRootImpl是整个控件系统正常运转的动力所在,无疑是本章最关键的一个组件。

·  将控件、布局參数以及新建的ViewRootImpl以同样的索引值加入到三个相应的数组mViews、mParams以及mRoots中,以供之后的查询之需。

控件、布局參数以及ViewRootImpl三者共同组成了client的一个窗体。

或者说,在控件系统中的窗体就是控件、布局參数与ViewRootImpl对象的一个三元组。

注意 笔者并不认同将这个三元组分别存储在三个数组中的设计。

假设创建一个WindowRecord类来统一保存这个三元组将能够省去非常多麻烦。

另外,mViews、mParams以及mRoots这三个数组的容量是随着当前进程中的窗体数量的变化而变化的。因此在addView()以及随后的removeView()中都伴随着数组的新建、拷贝等操作。

鉴于一个进程所加入的窗体数量不会太多,而且也不会非常频繁,所以这些时间开销是能够接受的。只是笔者仍然觉得相对于数组。ArrayList或CopyOnWriteArrayList是更好的选择。

·  调用ViewRootImpl.setView()函数。将控件交给ViewRootImpl进行托管。

这个动作将使得ViewRootImpl向WMS加入窗体、获取Surface以及重绘等一系列的操作。

这一步是控件能够作为一个窗体显示在屏幕上的根本原因。

整体来说,WindowManagerGlobal在通过父窗体调整了布局參数之后。将新建的ViewRootImpl、控件以及布局參数保存在自己的三个数组中。然后将控件交由新建的ViewRootImpl进行托管。从而完毕了窗体的加入。WindowManagerGlobal管理窗体的原理如图6-3所看到的。

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />

图 6 - 3 WindowManagerGlobal的窗体管理

6.2.3 更新窗体的布局

ViewManager所定义的另外一个功能就是更新View的布局。

在WindowManager中,则是更新窗体的布局。

窗体的布局參数发生变化时,如LayoutParams.width从100变为了200,则须要将这个变化通知给WMS使其调整Surface的大小。并让窗体进行重绘。这个工作在WindowManagerGlobal中由updateViewLayout()函数完毕。

[WindowManagerGlobal.java-->WindowManagerGlobal.updateViewLayout()]

publicvoid updateViewLayout(View view, ViewGroup.LayoutParams params) {

......// 參数检查

final WindowManager.LayoutParams wparams =(WindowManager.LayoutParams)params;

// 将布局參数保存到控件中

view.setLayoutParams(wparams);

synchronized (mLock) {

// 获取窗体在三个数组中的索引

int index = findViewLocked(view, true);

ViewRootImpl root = mRoots[index];

// 更新布局參数到数组中

mParams[index] = wparams;

// 调用ViewRootImpl的setLayoutParams()使得新的布局參数生效

root.setLayoutParams(wparams, false);

}

}

更新窗体布局的工作在WindowManagerGlobal中是非常easy的,主要是保存新的布局參数,然后调用ViewRootImpl.setLayoutParams()进行更新。

6.2.3 删除窗体

接下来探讨窗体的删除操作。在了解了WindowManagerGlobal管理窗体的方式后应该能够非常easy地判断出删除窗体所须要做的工作:

·  从3个数组中删除此窗体所相应的元素,包括控件、布局參数以及ViewRootImpl。

·  要求ViewRootImpl从WMS中删除相应的窗体(IWindow)。并释放一切须要回收的资源。

这个过程十分简单,这里就不引用相关的代码了。仅仅是有一点须要说明一下:要求ViewRootImpl从WMS中删除窗体并释放资源的方法是调用ViewRootImpl.die()函数。因此能够得出这样一个结论:ViewRootImpl的生命从setView()開始。到die()结束。

6.2.4 WindowManager的总结

经过前文的分析。相信读者对WindowManager的工作原理有了深入的认识。

·  鉴于窗体布局和控件布局的一致性,WindowManager继承并实现了接口ViewManager。

·  使用者能够通过Context.getSystemService(Context.WINDOW_SERVICE)来获取一个WindowManager的实例。这个实例的真实类型是WindowManagerImpl。

WindowManagerImpl一旦被创建就确定了通过它所创建的窗体所属哪块屏幕?哪个父窗体?

·  WindowManagerImpl除了保存了窗体所属的屏幕以及父窗体以外,没有不论什么实质性的工作。窗体的管理都交由WindowManagerGlobal的实例完毕。

·  WindowManagerGlobal在一个进程中仅仅有一个实例。

·  WindowManagerGlobal在3个数组中统一管理整个进程中的全部窗体的信息。

这些信息包括控件、布局參数以及ViewRootImpl三个元素。

·  除了管理窗体的上述3个元素以外。WindowManagerGlobal将窗体的创建、销毁与布局更新等任务交付给了ViewRootImpl完毕。

说明 在实际的应用开发过程中,有时会在logcat的输出中遇到有关WindowLeaked的异常输出。WindowLeaked异常发生与WindowManagerGlobal中,其原因是Activity在destroy之前没有销毁其附属窗体,如对话框、弹出菜单等。

如此看来,WindowManager的实现仍然是非常轻量的。

窗体的创建、销毁与布局更新都指向了一个组件:ViewRootImpl。

6.3 深入理解ViewRootImpl

ViewRootImpl实现了ViewParent接口,作为整个控件树的根部,它是控件树正常运作的动力所在,控件的測量、布局、绘制以及输入事件的派发处理都由ViewRootImpl触发。还有一方面,它是WindowManagerGlobal工作的实际实现者,因此它还须要负责与WMS交互通信以调整窗体的位置大小,以及对来自WMS的事件(如窗体尺寸改变等)作出相应的处理。

本节将对ViewRootImpl的实现做深入的探讨。

6.3.1 ViewRootImpl的创建及其重要的成员

ViewRootImpl创建于WindowManagerGlobal的addView()方法中。而调用addView()方法的线程即是此ViewRootImpl所掌控的控件树的UI线程。ViewRootImpl的构造主要是初始化了一些重要的成员。事先对这些重要的成员有个初步的认识对随后探讨ViewRootImpl的工作原理有非常大的帮助。其构造函数代码例如以下:

[ViewRootImpl.java-->ViewRootImpl.ViewRootImpl()]

public ViewRootImpl(Context context, Displaydisplay) {

/* ① 从WindowManagerGlobal中获取一个IWindowSession的实例。它是ViewRootImpl和

WMS进行通信的代理 */

mWindowSession= WindowManagerGlobal.getWindowSession(context.getMainLooper());

// ②保存參数display,在后面setView()调用中将会把窗体加入到这个Display上

mDisplay= display;

CompatibilityInfoHolder cih = display.getCompatibilityInfo();

mCompatibilityInfo = cih != null ?

cih : new CompatibilityInfoHolder();

/* ③ 保存当前线程到mThread。

这个赋值操作体现了创建ViewRootImpl的线程怎样成为UI主线程。

在ViewRootImpl处理来自控件树的请求时(如请求又一次布局。请求重绘,改变焦点等)。会检

查发起请求的thread与这个mThread是否同样。

倘若不同则会拒绝这个请求并抛出一个异常*/

mThread= Thread.currentThread();

......

/* ④ mDirty用于收集窗体中的无效区域。所谓无效区域是指因为数据或状态发生改变时而须要进行重绘

的区域。举例说明,当应用程序改动了一个TextView的文字时,TextView会将自己的区域标记为无效

区域。并通过invalidate()方法将这块区域收集到这里的mDirty中。当下次绘制时,TextView便

能够将新的文字绘制在这块区域上 */

mDirty =new Rect();

mTempRect = new Rect();

mVisRect= new Rect();

/* ⑤ mWinFrame。描写叙述了当前窗体的位置和尺寸。与WMS中WindowState.mFrame保持着一致 */

mWinFrame = new Rect();

/* ⑥ 创建一个W类型的实例。W是IWindow.Stub的子类。

即它将在WMS中作为新窗体的ID。以及接

收来自WMS的回调*/

mWindow= new W(this);

......

/* ⑦ 创建mAttachInfo。

mAttachInfo是控件系统中非常重要的对象。

它存储了此当前控件树所以贴附

的窗体的各种实用的信息。而且会派发给控件树中的每个控件。这些控件会将这个对象保存在自己的

mAttachInfo变量中。

mAttachInfo中所保存的信息有WindowSession。窗体的实例(即mWindow),

ViewRootImpl实例,窗体所属的Display,窗体的Surface以及窗体在屏幕上的位置等等。

所以,当

要需在一个View中查询与当前窗体相关的信息时,非常值得在mAttachInfo中搜索一下 */

mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display,this, mHandler, this);

/* ⑧ 创建FallbackEventHandler。这个类如同PhoneWindowManger一样定义在android.policy

包中,事实上现为PhoneFallbackEventHandler。FallbackEventHandler是一个处理未经不论什么人

消费的输入事件的场所。在6.5.4节中将会介绍它 */

mFallbackEventHandler =PolicyManager.makeNewFallbackEventHandler(context);

......

/* ⑨ 创建一个依附于当前线程,即主线程的Choreographer,用于通过VSYNC特性安排重绘行为 */

mChoreographer= Choreographer.getInstance();

......

}

在构造函数之外,还有另外两个重要的成员被直接初始化:

·  mHandler,类型为ViewRootHandler,一个依附于创建ViewRootImpl的线程,即主线程上的,用于将某些必须主线程进行的操作安排在主线程中运行。mHandler与mChoreographer的同一时候存在看似有些反复。事实上它们拥有明白不同的分工与意义。因为mChoreographer处理消息时具有VSYNC特性,因此它主要用于处理与重绘相关的操作。可是因为mChoreographer须要等待VSYNC的垂直同步事件来触发对下一条消息的处理,因此它处理消息的及时性稍逊于mHandler。而mHandler的作用。则是为了将发生在其它线程中的事件安排在主线程上运行。所谓发生在其它线程中的事件是指来自于WMS,由继承自IWindow.Stub的mWindow引发的回调。因为mWindow是一个Binder对象的Bn端,因此这些回调发生在Binder的线程池中。而这些回调会影响到控件系统的又一次測量、布局与绘制,因此须要此Handler将回调安排到主线程中。

说明 mHandler与mThread两个成员都是为了单线程模型而存在的。Android的UI操作不是线程安全的,而且非常多操作也是建立在单线程的假设之上(如scheduleTraversals())。

採用单线程模型的目的是减少系统的复杂度,而且减少锁的开销。

·  mSurface,类型为Surface。採用无參构造函数创建的一个Surface实例。mSurface此时是一个没有不论什么内容的空壳子。在 WMS通过relayoutWindow()为其分配一块Surface之前尚不能实用。

·  mWinFrame、mPendingContentInset、mPendingVisibleInset以及mWidth,mHeight。这几个成员存储了窗体布局相关的信息。

当中mWinFrame、mPendingConentInsets、mPendingVisibleInsets与窗体在WMS中的Frame、ContentInsets、VisibleInsets是保持同步的。

这是因为这3个成员不仅会作为 relayoutWindow()的传出參数,而且ViewRootImpl在收到来自WMS的回调IWindow.Stub.resize()时,马上更新这3个成员的取值。因此这3个成员体现了窗体在WMS中的最新状态。
与mWinFrame中的记录窗体在WMS中的尺寸不同的是,mWidth/mHeight记录了窗体在ViewRootImpl中的尺寸。二者在绝大多数情况下是同样的。当窗体在WMS中被又一次布局而导致尺寸发生变化时,mWinFrame会首先被IWindow.Stub.resize()回调更新,此时mWinFrame便会与mWidth/mHeight产生差异。

此时ViewRootImpl就可以得知须要对控件树进行又一次布局以适应新的窗体变化。在布局完毕后,mWidth/mHeight会被赋值为mWinFrame中所保存的宽和高。二者又一次统一。在随后分析performTraversals()方法时。读者将会看到这一处理。

另外,与mWidth/mHeight相似,ViewRootImpl也保存了窗体的位置信息Left/Top以及ContentInsets/VisibleInsets供控件树查询,只是这四项信息被保存在了mAttachInfo中。

ViewRootImpl的在其构造函数中初始化了一系列的成员变量。然而其创建过程仍未完毕。仅在为其指定了一个控件树进行管理。并向WMS加入了一个新的窗体之后,ViewRootImpl承上启下的角色才算全然确立下来。因此须要进一步分析ViewRootImpl.setView()方法。

[ViewRootImp.java-->ViewRootImpl.setView()]

public void setView(View view,WindowManager.LayoutParams attrs, View panelParentView) {

synchronized (this) {

if (mView == null) {

// ① mView保存了控件树的根

mView = view;

......

// ②mWindowAttributes保存了窗体所相应的LayoutParams

mWindowAttributes.copyFrom(attrs);

......

/* 在加入窗体之前,先通过requestLayout()方法在主线程上安排一次“遍历”。所谓

“遍历”是指ViewRootImpl中的核心方法performTraversals()。

这种方法实现了对

控件树进行測量、布局、向WMS申请改动窗体属性以及重绘的全部工作。因为此“遍历”

操作对于初次遍历做了一些特殊处理。而来自WMS通过mWindow发生的回调会导致一些属性

发生变化。如窗体的尺寸、Insets以及窗体焦点等。从而有可能使得初次“遍历”的现场遭

到破坏。因此,须要在加入窗体之前。先发送一个“遍历”消息到主线程。

在主线程中向主线程的Handler发送消息假设使用得当。能够产生非常精妙的效果。比如本例

中能够实现例如以下的运行顺序:加入窗体->初次遍历->处理来自WMS的回调 */

requestLayout();

/*③ 初始化mInputChannel。參考第五章,InputChannel是窗体接受来自InputDispatcher

的输入事件的管道。 注意,仅当窗体的属性inputFeatures不含有

INPUT_FEATURE_NO_INPUT_CHANNEL时才会创建InputChannel,否则mInputChannel

为空,从而导致此窗体无法接受不论什么输入事件 */

if ((mWindowAttributes.inputFeatures

& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {

mInputChannel = new InputChannel();

}

try {

......

/* 将窗体加入到WMS中。完毕这个操作之后。mWindow已经被加入到指定的Display中去

而且mInputChannel(假设不为空)已经准备好接受事件了。

仅仅是因为这个窗体没有进行

过relayout(),因此它还没有有效的Surface能够进行绘制 */

res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,

getHostVisibility(), mDisplay.getDisplayId(),

mAttachInfo.mContentInsets, mInputChannel);

} catch (RemoteException e) {......} finally { ...... }

......

if (res < WindowManagerGlobal.ADD_OKAY) {

// 错误处理。窗体加入失败的原因一般是权限问题。反复加入,或者tokeen无效

}

......

/*④ 假设mInputChannel不为空,则创建mInputEventReceiver,用于接受输入事件。

注意第二个參数传递的是Looper.myLooper(),即mInputEventReceiver将在主线程上

触发输入事件的读取与onInputEvent()。

这是应用程序能够在onTouch()等事件响应中

直接进行UI操作等根本原因。

*/

if (mInputChannel != null) {

......

mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,

Looper.myLooper());

}

/* ViewRootImpl将作为參数view的parent。

所以,ViewRootImpl能够从控件树中不论什么一个

控件開始。通过回溯getParent()的方法得到 */

view.assignParent(this);

......

}

}

}

至此,ViewRootImpl全部重要的成员都已经初始化完毕,新的窗体也已经加入到WMS中。ViewRootImpl的创建过程是由构造函数和setView()方法两个环节构成的。当中构造函数主要进行成员的初始化,setView()则是创建窗体、建立输入事件接收机制的场所。同一时候,触发第一次“遍历”操作的消息已经发送给主线程。在随后的第一次“遍历”完毕后。ViewRootImpl将会完毕对控件树的第一次測量、布局,并从WMS获取窗体的Surface以进行控件树的初次绘制工作。

在本节的最后,通过图 6 – 4对ViewRootImpl中的重要成员进行了分类整理。

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />

图 6 - 4 ViewRootImpl中的主要成员

6.3.2 控件系统的心跳:performTraversals()

ViewRootImpl在其创建过程中通过requestLayout()向主线程发送了一条触发“遍历”操作的消息。“遍历”操作是指performTraversals()方法。

它的性质与WMS中的performLayoutAndPlaceSurfacesLocked()相似,是一个包罗万象的方法。ViewRootImpl中接收到的各种变化。如来自WMS的窗体属性变化,来自控件树的尺寸变化、重绘请求等都引发performTraversals()的调用,并在当中完毕处理。

View类及其子类中的onMeasure()、onLayout()以及onDraw()等回调也都是在performTraversals()的运行过程中直接或间接地引发。

也正是如此。一次次的performTraversals()调用驱动着控件树有条不紊地工作着,一旦此方法无法正常运行,整个控件树都将处于僵死状态。因此,performTraversals()函数可谓是ViewRootImpl的心跳。

因为布局的相关工作是此方法中最主要的内容。为了简化分析,并突出此方法的工作流程,本节将以布局的相关工作为主线进行探讨。待完毕了这部分内容的分析之后,庞大的performTraversals()方法将不再那么难以驯服,读者便能够轻易地学习其它的工作了。

1.performTraversals()的工作阶段

performTraversals()是Android 源代码中最庞大的方法之中的一个,因此在正式探讨它的实现之前最好先将其划分为下面几个工作阶段作为指导。

·  预測量阶段。这是进入performTraversals()方法后的第一个阶段,它会对控件树进行第一次測量。測量结果能够通过mView. getMeasuredWidth()/Height()获得。在此阶段中将会计算出控件树为显示其内容所需的尺寸,即期望的窗体尺寸。

在这个阶段中。View及其子类的onMeasure()方法将会沿着控件树依次得到回调。

·  布局窗体阶段。

依据预測量的结果,通过IWindowSession.relayout()方法向WMS请求调整窗体的尺寸等属性,这将引发WMS对窗体进行又一次布局,并将布局结果返回给ViewRootImpl。

·  终于測量阶段。预測量的结果是控件树所期望的窗体尺寸。然而因为在WMS中影响窗体布局的因素非常多(參考第4章),WMS不一定会将窗体准确地布局为控件树所要求的尺寸,而迫于WMS作为系统服务的强势地位,控件树不得不接受WMS的布局结果。因此在这一阶段,performTraversals()将以窗体的实际尺寸对控件进行终于測量。在这个阶段中,View及其子类的onMeasure()方法将会沿着控件树依次被回调。

·  布局控件树阶段。完毕终于測量之后便能够对控件树进行布局了。

測量确定的是控件的尺寸,而布局则是确定控件的位置。在这个阶段中,View及其子类的onLayout()方法将会被回调。

·  绘制阶段。这是performTraversals()的终于阶段。确定了控件的位置与尺寸后,便能够对控件树进行绘制了。在这个阶段中,View及其子类的onDraw()方法将会被回调。

说明 非常多文章都倾向于将performTraversals()的工作划分为測量、布局与绘制三个阶段。

然而笔者觉得如此划分隐藏了WMS在这个过程中的地位,而且没能体现出控件树对窗体尺寸的期望、WMS对窗体尺寸做终于的确定。最后以WMS给出的结果为准再次进行測量的协商过程。

而这个协商过程充分体现了ViewRootImpl作为WMS与控件树的中间人的角色。

接下来将结合代码。对上述五个阶段进行深入的分析。

2.预測量与測量原理

本节将探讨performTraversals()将以何种方式对控件树进行预測量。同一时候,本节也会对控件的測量过程与原理进行介绍。

预測量參数的候选

预測量也是一次完整的測量过程。它与终于測量的差别仅在于參数不同而已。实际的測量工作在View或其子类的onMeasure()方法中完毕,而且其測量结果须要受限于来自其父控件的指示。这个指示由onMeasure()方法的两个參数进行传达:widthSpec与heightSpec。

它们是被称为MeasureSpec的复合整型变量,用于指导控件对自身进行測量。

它有两个分量,结构如图6-5所看到的。

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />

图 6 - 5 MeasureSpec的结构

其1到30位给出了父控件建议尺寸。建议尺寸对測量结果的影响依不同的SPEC_MODE的不同而不同。

SPEC_MODE的取值取决于此控件的LayoutParams.width/height的设置,能够是例如以下三种值之中的一个。

·  MeasureSpec.UNSPECIFIED (0):表示控件在进行測量时,能够无视SPEC_SIZE的值。控件能够是它所期望的随意尺寸。

·  MeasureSpec.EXACTLY (1):表示子控件必须为SPEC_SIZE所制定的尺寸。当控件的LayoutParams.width/height为一确定值,或者是MATCH_PARENT时,相应的MeasureSpec參数会使用这个SPEC_MODE。

·  MeasureSpec.AT_MOST (2):表示子控件能够是它所期望的尺寸,可是不得大于SPEC_SIZE。当控件的LayoutParams.width/height为WRAP_CONTENT时。相应的MeasureSpec參数会使用这个SPEC_MODE。

Android提供了一个MeasureSpec类用于组合两个分量成为一个MeasureSpec。或者从MeasureSpec中分离不论什么一个分量。

那么ViewRootImpl会怎样为控件树的根mView准备其MeasureSpec呢?

參考例如以下代码,注意desiredWindowWidth/Height的取值。它们将是SPEC_SIZE分量的候选。另外,这段代码分析中也解释了与測量无关。可是比較重要的代码段。

[ViewRootImpl.java-->ViewRootImpl.performTraversals()]

private void performTraversals() {

// 将mView保存在局部变量host中,以此提高对mView的訪问效率

finalView host = mView;

......

// 声明本阶段的主角,这两个变量将是mView的SPEC_SIZE分量的候选

intdesiredWindowWidth;

intdesiredWindowHeight;

.......

Rectframe = mWinFrame; // 如上一节所述。mWinFrame表示了窗体的最新尺寸

if(mFirst) {

/*mFirst表示了这是第一次遍历,此时窗体刚刚被加入到WMS。此时窗体尚未进行relayout,因此

mWinFrame中没有存储有效地窗体尺寸 */

if(lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL) {

......// 为状态栏设置desiredWindowWidth/Height,其取值是屏幕尺寸

}else {

//① 第一次“遍历”的測量。採用了应用能够使用的最大尺寸作为SPEC_SIZE的候选

DisplayMetrics packageMetrics =

mView.getContext().getResources().getDisplayMetrics();

desiredWindowWidth = packageMetrics.widthPixels;

desiredWindowHeight = packageMetrics.heightPixels;

}

/* 因为这是第一次进行“遍历”,控件树即将第一次被显示在窗体上,因此接下来的代码填充了

mAttachInfo中的一些字段。然后通过mView发起了dispatchAttachedToWindow()的调用

之后每个位于控件树中的控件都会回调onAttachedToWindow() */

......

} else {

// ② 在非第一次遍历的情况下,会採用窗体的最新尺寸作为SPEC_SIZE的候选

desiredWindowWidth = frame.width();

desiredWindowHeight = frame.height();

/* 假设窗体的最新尺寸与ViewRootImpl中的现有尺寸不同,说明WMS側单方面改变了窗体的尺寸

这将产生例如以下三个结果 */

if(desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {

// 须要进行完整的重绘以适应新的窗体尺寸

mFullRedrawNeeded = true;

// 须要对控件树进行又一次布局

mLayoutRequested = true;

/* 控件树有可能拒绝接受新的窗体尺寸。比方在随后的预測量中给出了不同于窗体尺寸的測量结果

产生这样的情况时,就须要在窗体布局阶段尝试设置新的窗体尺寸 */

windowSizeMayChange = true;

}

}

......

/* 运行位于RunQueue中的回调。RunQueue是ViewRootImpl的一个静态成员。即是说它是进程唯一

的,而且能够在进程的不论什么位置訪问RunQueue。

在进行多线程任务时。开发人员能够通过调用View.post()

或View.postDelayed()方法将一个Runnable对象发送到主线程运行。这两个方法的原理是将

Runnable对象发送到ViewRootImpl的mHandler去。当控件已经加入到控件树时,能够通过

AttachInfo轻易获取这个Handler。

而当控件没有位于控件树中时,则没有mAttachInfo可用,此时

运行View.post()/PostDelay()方法,Runnable将会被加入到这个RunQueue队列中。

在这里,ViewRootImpl将会把RunQueue中的Runnable发送到mHandler中,进而得到运行。所以

不管控件是否显示在控件树中,View.post()/postDelay()方法都是可用的。除非当前进程中没有不论什么

处于活动状态的ViewRootImpl */

getRunQueue().executeActions(attachInfo.mHandler);

booleanlayoutRequested = mLayoutRequested && !mStopped;

/* 仅当layoutRequested为true时才进行预測量。

layoutRequested为true表示在进行“遍历”之前requestLayout()方法被调用过。

requestLayout()方法用于要求ViewRootImpl进行一次“遍历”并对控件树又一次进行測量与布局 */

if(layoutRequested) {

final Resources res = mView.getContext().getResources();

if(mFirst) {

......// 确定控件树是否须要进入TouchMode。本章将在6.5.1节介绍 TouchMode

}else {

/*检查WMS是否单方面改变了ContentInsets与VisibleInsets。

注意对二者的处理的差异,

ContentInsets描写叙述了控件在布局时必须预留的空间,这样会影响控件树的布局,因此将

insetsChanged标记为true,以此作为是否进行控件布局的条件之中的一个。

而VisibleInsets则

描写叙述了被遮挡的空间,ViewRootImpl在进行绘制时,须要调整绘制位置以保证关键控件或区域,

如正在进行输入的TextView等不被遮挡,这样VisibleInsets的变化并不会导致又一次布局。

所以这里仅仅是将VisibleInsets保存到mAttachInfo中,以便绘制时使用 */

if (!mPendingContentInsets.equals(mAttachInfo.mContentInsets)) {

insetsChanged = true;

}

if (!mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets)) {

mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);

}

/*当窗体的width或height被指定为WRAP_CONTENT时,表示这是一个悬浮窗体。

此时会对desiredWindowWidth/Height进行调整。

在前面的代码中,这两个值被设置

被设置为窗体的当前尺寸。而依据MeasureSpec的要求,測量结果不得大于SPEC_SIZE。

然而。假设这个悬浮窗体须要更大的尺寸以完整显示其内容时,比如为AlertDialog设置了

一个更长的消息内容,如此取值将导致无法得到足够大的測量结果,从而导致内容无法完整显示。

因此。对于此等类型的窗体,ViewRootImpl会调整desiredWindowWidth/Height为此应用

能够使用的最大尺寸 */

if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT

|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {

// 悬浮窗体的尺寸取决于測量结果。因此有可能须要向WMS申请改变窗体的尺寸。

windowSizeMayChange = true;

if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL) {

//

} else {

// ③ 设置悬浮窗体SPEC_SIZE的候选为应用能够使用的最大尺寸

DisplayMetrics packageMetrics = res.getDisplayMetrics();

desiredWindowWidth = packageMetrics.widthPixels;

desiredWindowHeight = packageMetrics.heightPixels;

}

}

}

// ④ 进行预測量。通过measureHierarchy()方法以desiredWindowWidth/Height进行測量

windowSizeMayChange |=measureHierarchy(host, lp, res,

desiredWindowWidth, desiredWindowHeight);

}

// 其它阶段的处理

......

由此可知,预測量时的SPEC_SIZE依照例如以下原则进行取值:

·  第一次“遍历”时,使用应用可用的最大尺寸作为SPEC_SIZE的候选。

·  此窗体是一个悬浮窗体。即LayoutParams.width/height当中之中的一个被指定为WRAP_CONTENT时,使用应用可用的最大尺寸作为SPEC_SIZE的候选。

·  在其它情况下,使用窗体最新尺寸作为SPEC_SIZE的候选。

最后,通过measureHierarchy()方法进行測量。

測量协商

measureHierarchy()用于測量整个控件树。传入的參数desiredWindowWidth与desiredWindowHeight在前述代码中依据不同的情况作了精心的挑选。控件树本能够依照这两个參数完毕測量。可是measureHierarchy()有自己的考量。即怎样将窗体布局地尽可能地优雅。

这是针对将LayoutParams.width设置为了WRAP_CONTENT的悬浮窗体而言。如前文所述。在设置为WRAP_CONTENT时,指定的desiredWindowWidth是应用可用的最大宽度,如此可能会产生如图6-6左图所看到的的丑陋布局。这样的情况较easy发生在AlertDialog中,当AlertDialog须要显示一条比較长的消息时,因为给予的宽度足够大,因此它有可能将这条消息以一行显示,并使得其窗体充满了整个屏幕宽度,在横屏模式下这样的布局尤为丑陋。

倘若能够对可用宽度进行适当的限制,迫使AlertDialog将消息换行显示,则产生的布局结果将会优雅得多,如图6-6右图所看到的。可是。倘若不分清红皂白地对宽度进行限制。当控件树真正须要足够的横向空间时。会导致内容无法显示全然。或者无法达到最佳的显示效果。

比如当一个悬浮窗体希望尽可能大地显示一张照片时就会出现这样的情况。

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统

图 6 - 6 丑陋的布局与优雅的布局

那么measureHierarchy()怎样解决这个问呢?它採取了与控件树进行协商的办法,即先使用measureHierarchy()所期望的宽度限制尝试对控件树进行測量,然后通过測量结果来检查控件树能否够在此限制下满足其充分显示内容的要求。倘若没能满足。则measureHierarchy()进行让步,放宽对宽度的限制,然后再次进行測量,再做检查。

倘若仍不能满足则再度进行让步。

參考代码例如以下:

[ViewRootImpl.java-->ViewRootImpl.measureHierarchy()]

private boolean measureHierarchy(final View host,final WindowManager.LayoutParams lp,

final Resources res, final int desiredWindowWidth,

final int desiredWindowHeight) {

intchildWidthMeasureSpec; // 合成后的用于描写叙述宽度的MeasureSpec

intchildHeightMeasureSpec; // 合成后的用于描写叙述高度的MeasureSpec

booleanwindowSizeMayChange = false; // 表示測量结果是否可能导致窗体的尺寸发生变化

booleangoodMeasure = false; // goodMeasure表示了測量能否满足控件树充分显示内容的要求

// 測量协商仅发生在LayoutParams.width被指定为WRAP_CONTENT的情况下

if(lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {

/* ① 第一次协商。

measureHierarchy()使用它最期望的宽度限制进行測量。

这一宽度限制定义为

一个系统资源。能够在frameworks/base/core/res/res/values/config.xml找到它的定义 */

res.getValue(com.android.internal.R.dimen.config_prefDialogWidth,mTmpValue, true);

intbaseSize = 0;

// 宽度限制被存放在baseSize中

if(mTmpValue.type == TypedValue.TYPE_DIMENSION) {

baseSize = (int)mTmpValue.getDimension(packageMetrics);

}

if(baseSize != 0 && desiredWindowWidth > baseSize) {

// 使用getRootMeasureSpec()函数组合SPEC_MODE与SPEC_SIZE为一个MeasureSpec

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);

childHeightMeasureSpec =

getRootMeasureSpec(desiredWindowHeight,lp.height);

//②第一次測量。由performMeasure()方法完毕

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

/* 控件树的測量结果能够通过mView的getmeasuredWidthAndState()方法获取。假设

控件树对这个測量结果不惬意,则会在返回值中加入MEASURED_STATE_TOO_SMALL位 */

if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL)

==0) {

goodMeasure = true; // 控件树对測量结果惬意,測量完毕

} else {

// ③ 第二次协商。

上次測量结果表明控件树觉得measureHierarchy()给予的宽度太小。

在此适当地放宽对宽度的限制。使用最大宽度与期望宽度的中间值作为宽度限制 */

baseSize = (baseSize+desiredWindowWidth)/2;

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);

// ④ 第二次測量

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

// 再次检查控件树是否满足此次測量

if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL)

== 0) {

goodMeasure = true; // 控件树对測量结果惬意,測量完毕

}

}

}

}

if(!goodMeasure) {

/* ⑤ 终于測量。

当控件树对上述两次协商的结果都不惬意时,measureHierarchy()放弃全部限制

做终于測量。

这一次将不再检查控件树是否惬意了,因为即便其不惬意,measurehierarchy()也没

有很多其它的空间供其使用了 */

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth,lp.width);

childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,lp.height);

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

/* 最后,假设測量结果与ViewRootImpl中当前的窗体尺寸不一致。则表明随后可能有必要进行窗体

尺寸的调整 */

if(mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight())

{

windowSizeMayChange = true;

}

}

// 返回窗体尺寸是否可能须要发生变化

returnwindowSizeMayChange;

}

显然,对于非悬浮窗体,即当LayoutParams.width被设置为MATCH_PARENT时,不存在协商过程,直接使用给定的desiredWindowWidth/Height进行測量就可以。

而对于悬浮窗体,measureHierarchy()能够连续进行两次让步。因而在最不利的情况下,在ViewRootImpl的一次“遍历”中,控件树须要进行三次測量,即控件树中的每个View.onMeasure()会被连续调用三次之多,如图6-7所看到的。所以相对于onLayout()。onMeasure()方法的对性能的影响比較大。

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统

图 6 - 7 协商測量的三次尝试

接下来通过performMeasure()看控件树怎样进行測量。

測量原理

performMeasure()方法的实现非常easy。它直接调用mView.measure()方法,将measureHierarchy()给予的widthSpec与heightSpec交给mView。

看下View.measure()方法的实现:

[View.java-->View.measure()]

public final void measure(int widthMeasureSpec,int heightMeasureSpec) {

/* 仅当给予的MeasureSpec发生变化。或要求强制又一次布局时。才会进行測量。

所谓强制又一次布局。是指当控件树中的一个子控件的内容发生变化时,须要进行又一次的測量和布局的情况

在这样的情况下,这个子控件的父控件(以及其父控件的父控件)所提供的MeasureSpec必然与上次測量

时的值同样。因而导致从ViewRootImpl到这个控件的路径上的父控件的measure()方法无法得到运行

进而导致子控件无法又一次測量其尺寸或布局。因此。当子控件因内容发生变化时。从子控件沿着控件树回溯

到ViewRootImpl,并依次调用沿途父控件的requestLayout()方法,在这种方法中。会在

mPrivateFlags中加入标记PFLAG_FORCE_LAYOUT。从而使得这些父控件的measure()方法得以顺利

运行,进而这个子控件有机会进行又一次測量与布局。这便是强制又一次布局的意义 */

if ((mPrivateFlags& PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||

widthMeasureSpec != mOldWidthMeasureSpec ||

heightMeasureSpec != mOldHeightMeasureSpec) {

/* ① 准备工作。从mPrivateFlags中将PFLAG_MEASURED_DIMENSION_SET标记去除。

PFLAG_MEASURED_DIMENSION_SET标记用于检查控件在onMeasure()方法中是否通过

调用setMeasuredDimension()将測量结果存储下来 */

mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

......

/* ② 对本控件进行測量 每个View子类都须要重载这种方法以便正确地对自身进行測量。

View类的onMeasure()方法仅仅依据背景Drawable或style中设置的最小尺寸作为

測量结果*/

onMeasure(widthMeasureSpec, heightMeasureSpec);

/* ③ 检查onMeasure()的实现是否调用了setMeasuredDimension()

setMeasuredDimension()会将PFLAG_MEASURED_DIMENSION_SET标记又一次加入

mPrivateFlags中。之所以做这样的检查。是因为onMeasure()的实现可能由开发人员完毕,

而在Android看来。开发人员是不可信的 */

if((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET)

!=PFLAG_MEASURED_DIMENSION_SET) {

throw new IllegalStateException(......);

}

// ④ 将PFLAG_LAYOUT_REQUIRED标记加入mPrivateFlags。

这一操作会对随后的布局操作放行

mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;

}

// 记录父控件给予的MeasureSpec,用以检查之后的測量操作是否有必要进行

mOldWidthMeasureSpec = widthMeasureSpec;

mOldHeightMeasureSpec = heightMeasureSpec;

}

从这段代码能够看出,View.measure()方法没有实现不论什么測量算法,它的作用在于引发onMeasure()的调用,并对onMeasure()行为的正确性进行检查。另外,在控件系统看来,一旦控件运行了測量操作。那么随后必须进行布局操作,因此在完毕測量之后。将PFLAG_LAYOUT_REQUIRED标记加入mPrivateFlags,以便View.layout()方法能够顺利进行。

onMeasure()的结果通过setMeasuredDimension()方法尽行保存。setMeasuredDimension()方法的实现例如以下:

[View.java-->View.setMeasuredDimension()]

protected final void setMeasuredDimension(intmeasuredWidth, int measuredHeight) {

/* ① 測量结果被分别保存在成员变量mMeasuredWidth与mMeasuredHeight中

mMeasuredWidth = measuredWidth;

mMeasuredHeight = measuredHeight;

// ② 向mPrivateFlags中加入PFALG_MEASURED_DIMENSION_SET。以此证明onMeasure()保存了測量结果

mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;

}

事实上现再简单只是。存储測量结果的两个变量能够通过getMeasuredWidthAndState()与getMeasuredHeightAndState()两个方法获得,就像ViewRootImpl.measureHierarchy()中所做的一样。

此方法尽管简单,但须要注意,与MeasureSpec相似,測量结果不仅仅是一个尺寸,而是一个測量状态与尺寸的复合整、变量。其0至30位表示了測量结果的尺寸。而31、32位则表示了控件对測量结果是否惬意,即父控件给予的MeasureSpec能否够使得控件完整地显示其内容。当控件对測量结果惬意时。直接将尺寸传递给setMeasuredDimension()就可以,注意要保证31、32位为0。倘若对測量结果不惬意。则使用View.MEASURED_STATE_TOO_SMALL | measuredSize 作为參数传递给setMeasuredDimension()以告知父控件对MeasureSpec进行可能的调整。

既然明白了onMeasure()的调用怎样发起,以及它怎样将測量结果告知父控件,那么onMeasure()方法应当怎样实现的呢?对于非ViewGroup的控件来说事实上现相对简单,仅仅要依照MeasureSpec的原则如实计算其所需的尺寸就可以。

而对于ViewGroup类型的控件来说情况则复杂得多,因为它不仅拥有自身须要显示的内容(如背景),它的子控件也是其须要測量的内容。因此它不仅须要计算自身显示内容所需的尺寸,还有考虑其一系列子控件的測量结果。为此它必须为每个子控件准备MeasureSpec,并调用每个子控件的measure()函数。

因为各种控件所实现的效果形形色色,开发人员还能够依据需求自行开发新的控件。因此onMeasure()中的測量算法也会变化万千。不从Android系统实现的角度仍能得到例如以下的onMeasure()算法的一些实现原则:

·  控件在进行測量时,控件须要将它的Padding尺寸计算在内,因为Padding是其尺寸的一部分。

·  ViewGroup在进行測量时,须要将子控件的Margin尺寸计算在内。

因为子控件的Margin尺寸是父控件尺寸的一部分。

·  ViewGroup为子控件准备MeasureSpec时,SPEC_MODE应取决于子控件的LayoutParams.width/height的取值。取值为MATCH_PARENT或一个确定的尺寸时应为EXACTLY。WRAP_CONTENT时应为AT_MOST。

至于SPEC_SIZE,应理解为ViewGroup对子控件尺寸的限制。即ViewGroup依照事实上现意图所同意子控件获得的最大尺寸。而且须要扣除子控件的Margin尺寸。

·  尽管说測量的目的在于确定尺寸,与位置无关。可是子控件的位置是ViewGroup进行測量时必须要首先考虑的。因为子控件的位置即决定了子控件可用的剩余尺寸。也决定了父控件的尺寸(当父控件的LayoutParams.width/height为WRAP_CONTENT时)。

·  在測量结果中加入MEASURED_STATE_TOO_SMALL须要做到实事求是。当一个方向上的空间不足以显示其内容时应考虑利用还有一个方向上的空间,比如对文字进行换行处理,因为加入这个标记有可能导致父控件对其进行又一次測量从而减少效率。

·  当子控件的測量结果中包括MEASURED_STATE_TOO_SMALL标记时。仅仅要有可能,父控件就应当调整给予子控件的MeasureSpec,并进行又一次測量。倘若没有调整的余地。父控件也应当将MEASURED_STATE_TOO_SMALL加入到自己的測量结果中,让它的父控件尝试进行调整。

·  ViewGroup在測量子控件时必须调用子控件的measure()方法,而不能直接调用其onMeasure()方法。直接调用onMeasure()方法的最严重后果是子控件的PFLAG_LAYOUT_REQUIRED标识无法加入到mPrivateFlag中。从而导致子控件无法进行布局。

综上所述。測量控件树的实质是測量控件树的根控件。完毕控件树的測量之后,ViewRootImpl便得知了控件树对窗体尺寸的需求。

确定是否须要改变窗体尺寸

接下来回到performTraversals()方法。在ViewRootImpl.measureHierarchy()运行完毕之后,ViewRootImpl了解了控件树所需的空间。

于是便可确定是否须要改变窗体窗体尺寸以便满足控件树的空间要求。前述的代码中多处设置windowSizeMayChange变量为true。

windowSizeMayChange仅表示有可能须要改变窗体尺寸。而接下来的这段代码则用来确定窗体是否须要改变尺寸。

[ViewRootImpl.java-->ViewRootImp.performTraversals()]

private void performTraversals() {

......// 測量控件树的代码

/* 标记mLayoutRequested为false。因此在此之后的代码中。倘若控件树中不论什么一个控件运行了

requestLayout(),都会又一次进行一次“遍历” */

if (layoutRequested) {

mLayoutRequested = false;

}

// 确定窗体是否确实须要进行尺寸的改变

booleanwindowShouldResize = layoutRequested && windowSizeMayChange

&& ((mWidth != host.getMeasuredWidth() || mHeight !=host.getMeasuredHeight())

|| (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&

frame.width() < desiredWindowWidth && frame.width() !=mWidth)

|| (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&

frame.height() < desiredWindowHeight && frame.height() !=mHeight));

}

确定窗体尺寸是否确实须要改变的条件看起来比較复杂,这里进行一下总结。先介绍必要条件:

·  layoutRequested为true。即ViewRootImpl.requestLayout()方法被调用过。

View中也有requestLayout()方法。当控件内容发生变化从而须要调整其尺寸时。会调用其自身的requestLayout(),而且此方法会沿着控件树向根部回溯。终于调用到ViewRootImp.requestLayout(),从而引发一次performTraversals()调用。

之所以这是一个必要条件。是因为performTraversals()还有可能因为控件须要重绘时被调用。当控件仅须要重绘而不须要又一次布局时(比如背景色或前景色发生变化时)。会通过invalidate()方法回溯到ViewRootImpl,此时不会通过performTraversals()触发performTraversals()调用,而是通过scheduleTraversals()进行触发。在这样的情况下layoutRequested为false,即表示窗体尺寸不需发生变化。

·  windowSizeMayChange为true。如前文所讨论的,这意味着WMS单方面改变了窗体尺寸而控件树的測量结果与这一尺寸有差异,或当前窗体为悬浮窗体,其控件树的測量结果将决定窗体的新尺寸。

在满足上述两个条件的情况下,下面两个条件满足其一:

·  測量结果与ViewRootImpl中所保存的当前尺寸有差异。

·  悬浮窗体的測量结果与窗体的最新尺寸有差异。

注意ViewRootImpl对是否须要调整窗体尺寸的判断是非常小心的。

第4章介绍WMS的布局子系统时以前介绍过,调整窗体尺寸所必须调用的performLayoutAndPlaceSurfacesLocked()函数会导致WMS对系统中的全部窗体新型又一次布局。而且会引发至少一个动画帧渲染。其计算开销相当之大。因此ViewRootImpl仅在必要时才会惊动WMS。

至此,预測量阶段完毕了。

总结

这一阶段的工作内容是为了给兴许阶段做參数的准备而且当中最重要的工作是对控件树的预測量。至此ViewRootImpl得知了控件树对窗体尺寸的要求。

另外,这一阶段还准备了兴许阶段所需的其它參数:

·  viewVisibilityChanged。即View的可见性是否发生了变化。因为mView是窗体的内容,因此mView的可见性即是窗体的可见性。当这一属性发生变化时,须要通过通过WMS改变窗体的可见性。

LayoutParams。预測量阶段须要收集应用到LayoutParams的改动,这些改动一方面来自于WindowManager.updateViewLayout()。而还有一方面则来自于控件树。

以SystemUIVisibility为例,View.setSystemUIVisibility()所改动的设置须要反映到LayoutParams中。而这些设置确却保存在控件自己的成员变量里。在预測量阶段会通过ViewRootImpl.collectViewAttributes()方法遍历控件树中的全部控件以收集这些设置,然后更新LayoutParams。