深入理解控件(ViewRoot)系统(节选)
深入理解控件(ViewRoot)系统(节选)本章主要内容:· 介绍创建窗口的新的方法以及WindowManager的实现原理· 探讨ViewRootImpl的工作方式· 讨论控件树的测量、布局与绘制· 讨论输入事件在控件树中的派发· 介绍PhoneWindow的工作原理以及Activity窗口的创建方式本章涉及的源代码文件名及位置:· Conte
深入理解控件(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元素,尽管名称可能有所差异。例如文本框、图片框、列表框、组合框、按钮、单选按钮、多选按钮,等等。Android的控件系统不仅延续了对各种标准UI元素的支持,还针对移动平台的操作特点增加了使用更加方便、种类更加丰富的一系列新型的UI元素。
注意 在Android中,一个UI元素被称为一个视图(View),然而,笔者认为控件才是UI元素的更贴切的名字。因为UI元素不仅仅是为了向用户显示一些内容,更重要的是它们响应用户的输入并进行相应的工作。本书后续部分将以控件来称呼UI元素(View)。
另外,本章的目的并不是介绍如何使用各种Android控件,而是介绍Android控件系统的工作原理。本章要求读者至少应了解使用Android控件的基本知识。
读者所熟知的Activity、各种对话框、弹出菜单、状态栏与导航栏等等都是基于这套控件系统实现的。因此控件系统将是继WMS与IMS两大系统服务之后的又一个需要我们攻克的目标。
6.1.1 另一种创建窗口的方法
在这一小节里将介绍另外一种创建窗口的方法,并以此为切入点来开始对Android控件系统的探讨。
这个例子将会在屏幕中央显示一个按钮,它会浮在所有应用之上,直到用户点击它为止。市面上某些应用的悬浮窗就是如此实现的。
· 首先,读者使用Eclipse建立一个新的Android工程,并新建一个Service。然后在这个Service中增加如下代码:
// 将按钮作为一个窗口添加到WMS中
private void installFloatingWindow() {
// ① 获取一个WindowManager实例
finalWindowManager wm =
(WindowManager)getSystemService(Context.WINDOW_SERVICE);
// ② 新建一个按钮控件
finalButton btn = new Button(this.getBaseContext());
btn.setText("Click me to dismiss!");
// ③ 生成一个WindowManager.LayoutParams,用以描述窗口的类型与位置信息
LayoutParams lp = createLayoutParams();
// ④ 通过WindowManager.addView()方法将按钮作为一个窗口添加到系统中
wm.addView(btn, lp);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// ⑤当用户点击按钮时,将按钮从系统中删除
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!”的按钮浮在其他应用之上。而点击这个按钮后,它便消失了。
图 6 - 1浮动窗口例子的运行效果
读者可以将本例与第4章的例子SampleWindow做一个对比。它们的实现效果是大同小异的。而然,本章的这个例子无论是从最终效果、代码量、API的复杂度或可读性上都有很大的优势。这得益于对控件系统的使用。在这里,控件Button托管了窗口的绘制过程,并且将输入事件封装为了更具可读性的回调。并且添加窗口时所使用的WindowManager实例掩盖了客户端与WMS交互的复杂性。更重要的是,本例所使用的接口都来自公开的API,也就是说可以脱离Android源码进行编译。这无疑会带来更方便的开发过程以及更好的程序兼容性。
因此,除非需要进行很底层的窗口控制,使用本例所介绍的方法向系统中添加窗口是最优的选择。
6.1.2 控件系统的组成
从这个例子中可以看到在添加窗口过程中的两个关键组件:Button和WindowManager。Button是控件的一种,继承自View类。不只Button,任何一个继承自View类的控件都可以作为一个窗口添加到系统中去。WindowManager其实是一个继承自ViewManager的接口,它提供了添加/删除窗口,更新窗口布局的API,可以看作是WMS在客户端的代理类。不过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显示在屏幕上时包含了标题栏、菜单按钮等控件,但是在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所示。
图 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三者共同组成了客户端的一个窗口。或者说,在控件系统中的窗口就是控件、布局参数与ViewRootImpl对象的一个三元组。
注意 笔者并不认同将这个三元组分别存储在三个数组中的设计。如果创建一个WindowRecord类来统一保存这个三元组将可以省去很多麻烦。
另外,mViews、mParams以及mRoots这三个数组的容量是随着当前进程中的窗口数量的变化而变化的。因此在addView()以及随后的removeView()中都伴随着数组的新建、拷贝等操作。鉴于一个进程所添加的窗口数量不会太多,而且也不会很频繁,所以这些时间开销是可以接受的。不过笔者仍然认为相对于数组,ArrayList或CopyOnWriteArrayList是更好的选择。
· 调用ViewRootImpl.setView()函数,将控件交给ViewRootImpl进行托管。这个动作将使得ViewRootImpl向WMS添加窗口、获取Surface以及重绘等一系列的操作。这一步是控件能够作为一个窗口显示在屏幕上的根本原因!
总体来说,WindowManagerGlobal在通过父窗口调整了布局参数之后,将新建的ViewRootImpl、控件以及布局参数保存在自己的三个数组中,然后将控件交由新建的ViewRootImpl进行托管,从而完成了窗口的添加。WindowManagerGlobal管理窗口的原理如图6-3所示。
图 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中是非常简单的,主要是保存新的布局参数,然后调用ViewRootImpl.setLayoutParams()进行更新。
6.2.3 删除窗口
接下来探讨窗口的删除操作。在了解了WindowManagerGlobal管理窗口的方式后应该可以很容易地推断出删除窗口所需要做的工作:
· 从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中的重要成员进行了分类整理。
图 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所示。
图 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左图所示的丑陋布局。这种情况较容易发生在AlertDialog中,当AlertDialog需要显示一条比较长的消息时,由于给予的宽度足够大,因此它有可能将这条消息以一行显示,并使得其窗口充满了整个屏幕宽度,在横屏模式下这种布局尤为丑陋。
倘若能够对可用宽度进行适当的限制,迫使AlertDialog将消息换行显示,则产生的布局结果将会优雅得多,如图6-6右图所示。但是,倘若不分清红皂白地对宽度进行限制,当控件树真正需要足够的横向空间时,会导致内容无法显示完全,或者无法达到最佳的显示效果。例如当一个悬浮窗口希望尽可能大地显示一张照片时就会出现这样的情况。
图 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()方法的对性能的影响比较大。
图 6 - 7 协商测量的三次尝试
接下来通过performMeasure()看控件树如何进行测量。
测量原理
performMeasure()方法的实现非常简单,它直接调用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。开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)