深入理解Android:卷Ⅲ(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-23 18:55:18

点击下载

作者:张大伟

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

深入理解Android:卷Ⅲ

深入理解Android:卷Ⅲ试读:

前言

Preface

本书的主要内容及特色

本书是“深入理解Android”系列的第三本,也是完结篇。按照“深入理解Android”系列图书的路线图,本书所关注的重点是Android中有关用户交互的Framework的知识。总体来说,本书所涵盖的内容分为两个部分: 第一部分是对构成Android用户交互基础的

WindowManagerService、输入系统以及控件系统的介绍。 第二部分是以StatusBarManagerService、

NotificationManagerService以及Wallpaper-ManagerService为

例,对Android在第一部分内容基础之上所实现的UI相关的服务

进行探讨。

具体内容如下: 第1章介绍进行Android分析的一些准备工作,包括如何获取与编

译代码,使用IDE进行代码的阅读及调试等。 第2章,根据邓凡平的建议,由《深入理解Android:卷II》第2章

内容升级而来,将Android升级到4.2.2版本,并增加了与AIDL相

关的内容。Binder与MessageQueue是Android进程间通信与任务

调度的重要工具。因此,进行Android的深入研究之前理解这两

个工具的工作原理十分重要。 第3章主要介绍与AudioService服务相关的内容,包括音量控

制、AudioFocus以及音量控制面板等内容。 第4章介绍WindowManagerService的工作原理,其中涵盖与窗口

的创建、布局及动画相关的知识。 第5章介绍Android输入系统的工作原理,主要探讨输入事件的监

听、读取、翻译、封装以及派发循环等内容。 第6章介绍Android控件系统的工作原理,包括控件系统的测量、

布局、绘制、动画以及输入事件的派发。 第7章主要介绍与SystemUI相关的内容,其中包括

StatusBarManagerService与NotificationManagerService两个系统

服务,以及与状态栏和导航栏有关的知识。 第8章介绍与Android壁纸相关的内容,包括

WallpaperManagerService系统服务、动态壁纸与静态壁纸的工

作原理。另外还介绍WindowManagerService对壁纸窗口的一些

特殊处理。

其中第1章和第2章是全书的基础。第3章的内容相对独立,主要介绍与用户交互直接相关的音频方面的知识。而第4~6章是本书的重点内容,介绍Android UI的通用实现。在深入理解这三章的知识之后,读者可以通过借鉴第7章和第8章所介绍的SystemUI与壁纸的架构来提高Android与用户进行交互的深度定制能力。另外,Android源代码作为一个优秀的开源项目,大到架构的设计,小到某段代码的实现,都包含值得我们细细品味与吸收的设计思想,并且可以应用于自己所设计的代码上。因此,本书在代码分析的过程中尽可能地给出Android采用某种特定实现的原因或对其优秀的设计思路进行提取,希望读者能够知其然更知其所以然,进而能够在代码研究的过程中跳出代码的具体实现来体会其设计思想,而这正是本书根本目的所在。

读者对象 Android应用开发者。

通过本书可以理解SDK中与用户交互相关的API或工具的工作原理,而拥有这部分知识有助于应用开发者设计出更健壮、更有效率,而且更加细腻的代码实现。 Android系统开发工程师。

Android系统开发工程师将是本书所面向的最主要的读者群。同“深入理解Android”系列的其他书籍一样,本书将为这些读者提供其最感兴趣的系统实现方面的内容。 对Android系统的运行原理感兴趣的读者。

Android系统源代码中所体现的设计思想并不仅仅局限于Android,它对Android以外的开发工作同样极具借鉴意义。

如何阅读本书

本书所讨论的Android版本号为4.2.2,读者可以通过本书第1章所介绍的方法获取或在线阅读此版本的源代码。因为版本差异可能会使得某些源文件与类定义的位置发生变化,读者可以通过IDE集成的代码搜索功能进行查找。截至本书结稿之日,Android的最新版本为L,即5.0。在这个版本中,与输入系统相关的代码从frameworks/base/services/input文件夹移动到frameworks/native/services/inputflinger中,但本书所介绍的内容在这个版本中仍然适用。

读者需要注意,自第4章起,后一章的部分内容会以上一章为基础,尤其是第4~6章。虽说更加关注某一部分的读者可以直接阅读相关章节,但是笔者建议在阅读过程中至少先完成第4章有关窗口管理与布局内容的学习,因为这部分知识是后续内容的基础中的基础。

本书沿用了“深入理解Android”系列图书的代码引用风格,即在每章的开篇给出所有引用代码的完整路径,并在引用某一段代码之前指明这段代码来自哪个文件、哪个类的哪个方法(或函数),并以注释的方式对代码中的知识点进行介绍。如下所示:

另外,作为“深入理解Android”系列图书的一员,本书的内容与卷I、卷II有一定的联系,例如卷I的Surface系统、卷II的ActivityManagerService等在本书都会有所提及。读者可以将其作为本书的补充资料。

勘误和支持

由于本书涉及的内容及代码量巨大而且复杂,加之笔者的水平限制,书中难免会有一些不准确甚至错误的地方,还望各位读者不吝批评指正。另外,Android仍处在快速发展的过程中,卷III的成书也绝不是笔者对Android系统深入研究的一个句号。因此,倘若读者有关于本书的任何问题或建议,都可以与笔者进行讨论。

致谢

首先要感谢华章公司的杨福川以及本书的编辑姜影,拙稿得以付梓离不开他们耐心的支持与细致的校正,在此向他们致以最诚挚的感谢与敬意!还要感谢邓凡平在本书编写过程中给予的指导与建议,更要感谢他一直以来给予我的帮助与信任,能够为“深入理解Android”系列图书贡献一份力量真是我莫大的荣幸!

感谢我的妻子郭晓丽与我的家人。在我写书的过程中忽略了对他们的陪伴,而他们却给予了我一如既往的理解与支持。

还要感谢我在写作本书时所任职的中科创达与索尼移动通信两家公司的领导与同事,有幸与这样优秀的团队一起完成一个个富有挑战而又激动人心的工作让我由衷地感到开心,而他们给予我的信任与鼓励是我得以进步的最好动力。

还要感谢我的师长与朋友对我的关心与帮助,祝福他们!

最应当感谢的是关注本书的各位读者。倘若本书能够为各位的学习、工作尽些绵薄之力,这将是我最大的荣幸,而各位的意见、建议甚至批评则会是我努力的方向。第1章 开发环境部署

本章的主要内容: 介绍获取Android源代码的方法 介绍如何将Android源代码导入IDE中以方便阅读代码 介绍如何对Android的Java、C/C++源代码进行调试1.1 获取Android源代码

在深入研究Android之前,首先必须获得一套Android的源代码。Google提供官方Android源代码的获取方法如下:

https://source.android.com/source/downloading.html

这个页面介绍了使用repo脚本进行Android源码的下载的两个基本步骤。

1)首先通过repo init命令将当前文件夹初始化为repo脚本的工作区。其命令格式如下:

repo init命令会在当前文件夹下创建一个.repo文件夹,并从-u参数所指定的repository中下载一个manifest.xml文件到这个.repo文件夹。这个manifest.xml文件定义了Android源代码中所有git项目的清单,如下所示:

其中每一个project项都描述了一个git项目,而每一个git项目中则包含了负责某项功能的源代码。其中,name属性指定了git项目的名称,path属性指定了git项目将被下载到哪一个文件夹,而revision则指定了需要下载git项目的哪一个分支。上述manifest.xml片段中所给出的两个git项目分别存储了Android基本框架的代码以及Music应用程序的代码,它们将被分别下载到frameworks/base以及packages/apps/Music文件夹。

2)在完成repo工作区的初始化之后,便可以通过repo sync命令下载代码了。repo sync命令的原理就是解析.repo/manifest.xml中的内容,然后通过git工具逐个下载清单中所列举的git项目。repo sync可以接受-j参数进行多线程的代码下载以提高下载速度,例如repo sync-j8表示将使用8个线程。

由于整套Android源代码由数百个这样的git项目组成,因此进行Android源代码的完整下载是非常耗时的。倘若开发者只关心其中的某个部分,例如上述manifest.xml片段中所给出的Music应用程序的代码,那么可以这么做:

也就是说,在repo sync后面添加git项目的名称作为参数则可以单独下载这个项目的代码。在迫切地需要对Android的某个局部模块进行研究时,这个命令十分有用。说明其实manifest.xml也隶属于一个git项目,而这个git项目的名称与下载地址正是通过-u参数所指定的repository。所以通过在repo init中的-b参数指定的不同的分支可以获得不同内容的manifest.xml,进而repo sync得以下载不同的Android源代码。遗憾的是,Google官方所给出的repository所在的服务器在国内访问十分困难。除了使用官方提供的repository之外,一些芯片厂商提供的镜像repository可以用于源代码下载。通过官方服务器下载源代码遇到问题的读者可以在codeaurora.org以及omapzoom.org上找到用于下载Android源代码的镜像repository的地址。

倘若不需要进行代码编译及调试,那么在线阅读Android源代码无疑是一个非常方便的选择。基于OpenGrok代码搜索引擎的androidxref.com就是一个在线阅读Android源代码的站点。这个站点存储了自Android 1.6以来所有版本的Android源代码,并且在OpenGrok引擎的支持下可以非常快速地实现源代码的查找与跳转。如图1-1和图1-2所示。图1-1 使用AndroidXRef进行代码查找图1-2 使用AndroidXRef浏览代码1.2 Android的编译

在将下载到本地的代码添加到Eclipse或其他IDE之前,最好先进行一次完整的Android编译。这是因为某些代码文件是在编译过程中由aidl文件或资源文件所生成的,只有经过完整编译之后才能保证导入IDE中的Android源代码的完整性。

编译Android源代码非常简单,其步骤如下:

1)执行souce build/envsetup.sh,此命令将初始化Android的编译环境,并且声明一系列方便操作源代码的bash函数,如mmm、mm、cgrep、jgrep等。

2)输入lunch full-eng并执行。它是envsetup.sh中定义的一个函数,用于设置即将编译的项目以及类型。读者也可以通过等效的choosecombo命令对编译进行更精细设置。

3)输入make并执行Android编译。编译的中间结果以及最终产物(包括由aidl文件与资源所生成的代码文件)都存储在Android源代码根目录下的out文件夹中。1.3 在IDE中导入Android源代码

尽管Android的源代码并不依赖IDE进行编译,但是使用IDE进行代码的浏览、查找与跳转无疑是最方便的选择。本书所涉及的Android源代码主要是由Java语言以及C/C++语言编写的。对Java代码来说,Eclipse是最佳选择,而对C/C++代码来说,本书推荐使用速度更快的SourceInsight。1.3.1 将Android源代码导入Eclipse

首先需要将development/ide/eclipse/.classpath文件复制到源代码的根目录下。这个文件将在导入代码时告诉Eclipse在源代码的哪些文件夹中保存了Java代码,其内容片段如下:

这个xml文件由一系列classpathentry组成,每一个classpathentry指定了一个包含Java代码的文件夹,而Eclipse会从这些文件夹中查找并导入Java代码。这其中一批文件夹的路径以out文件夹打头,它们就是在Android编译过程中所产生的代码。倘若在没有进行过完整编译的情况下进行代码导入就会因为这些代码的丢失而使得某些引用无法解析。另外,倘若读者对某些文件夹下的代码不感兴趣,可以在进行代码导入前将它们从.classpath中注释掉以避免花费过长的导入时间以及过多的内存占用。就本书的内容而言,建议只保留framework相关的文件夹(注意,out文件夹下的framework相关文件夹也需要保留)。

修改完.classpath文件之后,便可以通过Eclipse下的菜单“File→New→Java Project”所打开的New Java Project对话框进行Android源代码的导入,如图1-3所示。在这个对话框中为项目取一个名字,然后将Location设置为存放Android源代码的根目录,然后点击Finish按钮就可以开始导入代码。这个过程会比较慢,读者需要耐心等待。

在导入完成之后,Android将会以一个Java工程的形式出现在Eclipse的Package Explorer中,接下来就可以在Eclipse中浏览代码了。图1-3 在Eclipse中导入Android源代码技巧熟悉一些常用的快捷键可以极大地提高代码的阅读效率。其中最常用的有:□CTRL+SHIFT+T组合键,跳转到一个类或接口的定义。□CTRL+O组合键,跳转到当前文件中所定义的一个成员(内部类、成员变量或方法等)。□CTRL+SHIFT+R组合键,跳转到一个特定的代码文件。□CTRL+SHIFT+G组合键,查找选中的元素在工作区中的引用位置。另外,可以在另一个Android程序的项目中将导入的Android源代码设置为一个依赖项目,这样就可以在APP代码中实现到Android Framework代码的无缝跳转。1.3.2 将Android源代码导入SourceInsight

相对于Eclipse,SourceInsight更适合用来阅读Android中的C/C++代码。读者可以从其官方网站上下载并获得30天的免费试用权。

将Android源代码导入SourceInsight非常简单。点击主菜单上的“Project→New Project”,在弹出的对话框中为新项目取一个名字然后点击OK按钮,如图1-4所示。图1-4 新建SourceInsight项目

在弹出的对话框的Project Source Directory中设置好Android源代码的根目录,点击OK按钮,如图1-5所示。图1-5 设置Android源代码的根目录

在新对话框中选中希望导入的代码所在的文件夹,然后点击Add(仅导入选中文件夹下的代码)或Add Tree(导入选中文件夹及其子文件夹下的代码)按钮,如图1-6所示,之后点击Close按钮结束代码的导入。图1-6 在SourceInsight中选择导入的代码路径

接下来就可以方便地在SourceInsight中阅读代码了。技巧在SourceInsight中追加导入或移除代码文件十分方便。只要点击主菜单上的“Project→Add and Remove Project Files”就可以打开如图1-6所示的对话框,然后进行代码的追加导入或删除。1.4 调试Android源代码

调试是分析问题与印证对代码的理解的最有效手段,对Android这种复杂而庞大的系统来说尤为如此。Android的源代码主要由Java代码以及C/C++代码构成,因此调试Android源代码需要从Java的调试以及C/C++的调试两个方面说起。1.4.1 使用Eclipse调试Android Java源代码

由于Android源代码是以一个普通的Java工程的方式导入的,于是在Eclipse中不能通过ADT所提供的DDMS直接对其进行远程调试。

1)首先需要通过DDMS获取调试进程的端口号。将设备通过USB连接PC,然后打开Eclipse的DDMS视图。在视图左侧Device进程列表中可以找到对应进程的调试端口号,如图1-7所示。图1-7 获取调试目标进程的端口号

2)回到Java视图,在Package Explorer中,用右键点击Android源代码所在的项目,在菜单中依次选择“Debug As→Debug Configurations”。在弹出的对话框中通过双击Remote Java Application新建一个远程调试配置。编辑Host为“localhost”,Port为调试进程的端口号之后点击Apply按钮保存配置。最后点击Debug按钮即可将Eclipse调试器绑定到对应的进程上,如图1-8所示。图1-8 启动对目标进程的远程调试

完成上述步骤后就可以通过Eclipse的Debug视图调试进程了。注意在点击Debug进行调试时,Eclipse可能会提示代码中存在错误(最常见的原因是注释掉了.classpatch中的代码路径而导致引用解析失败)。不过不用理会,直接点击Proceed按钮继续调试即可。导入Eclipse中的源代码可能运行在任何一个进程中,这份源代码可以对设备中任何一个Java进程进行调试。本书所介绍的各种系统服务均运行在system_server进程中,而其他的内容如SystemUI、WallpaperService则运行在其他的进程里。因此,读者需要注意正确选择调试目标进程。1.4.2 使用gdb调试Android C/C++源代码

下面介绍使用gdb调试C/C++代码的步骤。

首先通过adb shell ps获取需要进行调试的进程号,比如795。然后通过执行adb shell进入手机端的shell。

输入gdbserver:5039--attach795并执行。其中5039是端口号,795是待调试的进程号。于是gdbserver便会绑定在795进程上,并通过5039端口与PC端的调试器进行通信。

保持gdbserver运行,然后回到PC端执行adb forward tcp:5039 tcp:5039。这个命令可以在设备上的5039端口与PC上的5039端口之间建立一个映射。于是PC端的调试器可以通过本机的5039端口与设备上的gdbserver进行通信。

接下来便需要运行Android源代码中附带的复合设备及其架构的gdb工具,并连接到本机的5039端口进行调试。对ARM架构来说,这个工具为prebuilts/gcc/linux-x86/arm/arm-eabi-4.X/bin/arm-eabi-gdb。说明Android源代码中提供了用于ARM、x86以及MIPS等目标机器架构的编译工具链。倘若读者不清楚需要使用哪种机器架构下的编译工具链,可以先完成代码编译时source build/envsetup.sh以及choosecombo的执行以确定目标设备的类型。这样一来Android编译系统会将目标设备使用的机器架构对应的编译工具链所在的路径加入PATH环境变量中。然后就可以通过echo $PATH得知用于当前设备的编译工具链所在的路径,进而得知机器架构的类型。

进入gdb之后,依次执行如下命令:

之后便可以开始使用gdb的命令调试795进程了。技巧倘若读者觉得使用上述步骤进行调试比较繁复,可以使用Android源代码中所提供的gdbclient工具。gdbclient是定义在build/envsetup.sh中的一个Shell函数,它会根据choosecombo的结果判断可执行文件的路径以及so文件的路径,并自动完成上文所述的工作。因此,使用它之前必须完成source build/envsetup以及choosecombo的执行。使用gdbclient调试一个进程的方法如下:gdbclient<可执行文件名>:<端口号><进程号>例如,调试795的mediaserver进程可以使用如下命令:另外,倘若使用gdb调试Java进程中的C/C++代码,需要使用app_process作为可执行文件进行调试。1.5 本章小结

本章介绍了获取Android源代码、使用IDE进行源代码的阅读以及调试的方法。接下来让我们开始Android源代码的研究之旅吧。第2章 深入理解Java Binder和MessageQueue

本章主要内容: 介绍Binder系统的Java层框架 介绍MessageQueue

本章所涉及的源代码文件名及位置: IBinder.java

frameworks/base/core/java/android/os/IBinder.java Binder.java

frameworks/base/core/java/android/os/Binder.java BinderInternal.java

frameworks/base/core/java/com/android/intenal/os/BinderInternal.java android_util_Binder.cpp

frameworks/base/core/jni/android_util_Binder.cpp SystemServer.java

frameworks/base/services/java/com/android/servers/SystemServer.java ActivityManagerService.java

frameworks/base/services/java/com/android/servers/ActivityManagerService.java ServiceManager.java

frameworks/base/core/java/android/os/ServiceManager.java ServcieManagerNative.java

frameworks/base/core/java/android/os/ServcieManagerNative.java MessageQueue.java

frameworks/base/core/java/android/os/MessageQueue.java android_os_MessageQueue.cpp

frameworks/base/core/jni/android_os_MessageQueue.cpp Looper.cpp

frameworks/base/native/android/Looper.cpp Looper.h

frameworks/base/include/utils/Looper.h android_app_NativeActivity.cpp

frameworks/base/core/jni/android_app_NativeActivity.cpp2.1 概述

由于本书所介绍的内容是以Java层的系统服务为主,因此Binder相关的应用在本书中比比皆是。而MessageQueue作为Android中重要的任务调度工具,它的使用也是随处可见。所以本书有必要对这两个工具有所介绍。根据邓凡平的同意与推荐,本章由卷II第2章升级到4.2.2,并且增加了对AIDL相关知识点的分析。

本章作为本书Android源代码分析之旅的开篇,将重点关注两个基础知识点,它们是: Binder系统在Java世界是如何布局和工作的。 MessageQueue的新职责。

先来分析Java层中的Binder。建议读者先阅读《深入理解Android:卷I》(以下简称“卷I”)的第6章“深入理解Binder”。网上有样章可下载。2.2 Java层中的Binder分析2.2.1 Binder架构总览

如果读者读过卷I的第6章,相信就不会对Binder架构中代表Client的Bp端及代表Server的Bn端感到陌生。Java层中Binder实际上也是一个C/S架构,而且其在类的命名上尽量保持与Native层一致,因此可认为,Java层的Binder架构是Native层Binder架构的一个镜像。Java层的Binder架构中的成员如图2-1所示。

由图2-1可知:图2-1Java层中的Binder家族 系统定义了一个IBinder接口类以及DeathRecepient接口。 Binder类和BinderProxy类分别实现了IBinder接口。其中Binder类

作为服务端的Bn的代表,而BinderProxy作为客户端的Bp的代

表。 系统中还定义了一个BinderInternal类。该类是一个仅供Binder框

架使用的类。它内部有一个GcWatcher类,该类专门用于处理和

Binder相关的垃圾回收。 Java层同样提供一个用于承载通信数据的Parcel类。注意IBinder接口类中定义了一个叫FLAG_ONEWAY的整型变量,该变量的意义非常重要。当客户端利用Binder机制发起一个跨进程的函数调用时,调用方(即客户端)一般会阻塞,直到服务端返回结果。这种方式和普通的函数调用是一样的。但是在调用Binder函数时,在指明了FLAG_ONEWAY标志后,调用方只要把请求发送到Binder驱动即可返回,而不用等待服务端的结果,这就是一种所谓的非阻塞方式。在Native层中,涉及的Binder调用基本都是阻塞的,但是在Java层的framework中,使用FLAG_ONEWAY进行Binder调用的情况非常多,以后经常会碰到。思考使用FLAG_ONEWAY进行函数调用的程序在设计上有什么特点?这里简单分析一下:对使用FLAG_ONEWAY的函数来说,客户端仅向服务端发出请求,但是并不能确定服务端是否处理了该请求。所以,客户端一般会向服务端注册一个回调(同样是跨进程的Binder调用),一旦服务端处理了该请求,就会调用此回调来通知客户端处理结果。当然,这种回调函数也大多采用FLAG_ONEWAY的方式。2.2.2 初始化Java层Binder框架

虽然Java层Binder系统是Native层Binder系统的一个镜像,但这个镜像终归还需借助Native层Binder系统来开展工作,即镜像和Native层Binder有着千丝万缕的关系,一定要在Java层Binder正式工作之前建立这种关系。下面分析Java层Binder框架是如何初始化的。

在Android系统中,在Java初创时期,系统会提前注册一些JNI函数,其中有一个函数专门负责搭建Java Binder和Native Binder交互关系,该函数是register_android_os_Binder,代码如下:

据上面的代码可知,register_android_os_Binder函数完成了Java Binder架构中最重要的三个类的初始化工作。

1.Binder类的初始化

int_register_android_os_Binder函数完成了Binder类的初始化工作,代码如下:

由上面代码可知,gBinderOffsets对象保存了和Binder类相关的某些在JNI层中使用的信息。它们将用来在JNI层对Java层的Binder对象进行操作。execTransact()函数以及mObject成员的用途将在2.2.3节介绍。建议如果读者对JNI不是很清楚,可参阅卷I第2章“深入理解JNI”。

2.BinderInternal类的初始化

下一个初始化的类是BinderInternal,其代码在int_register_android_os_BinderInternal函数中。

int_register_android_os_BinderInternal的工作内容和int_register_android_os_Binder的工作内容类似: 获取一些有用的methodID和fieldID。这表明JNI层一定会向上调

用Java层的函数。 注册相关类中native函数的实现。

3.BinderProxy类的初始化

int_register_android_os_BinderProxy完成了BinderProxy类的初始化工作,代码稍显复杂,如下所示:

据上面代码可知,int_register_android_os_BinderProxy函数除了初始化BinderProxy类外,还获取了WeakReference类和Error类的一些信息。看来BinderProxy对象的生命周期会委托WeakReference来管理,所以JNI层会获取该类get函数的MethodID。

至此,Java Binder几个重要成员的初始化已完成,同时在代码中定义了几个全局静态对象,分别是gBinderOffsets、gBinderInternalOffsets和gBinderProxyOffsets。

框架的初始化其实就是提前获取一些JNI层的使用信息,如类成员函数的MethodID、类成员变量的fieldID等。这项工作是必需的,因为它能节省每次使用时获取这些信息的时间。当Binder调用频繁时,这些时间累积起来还是不容小觑的。

另外,这个过程中所创建的几个全局静态对象为JNI层访问Java层的对象提供了依据。而在每个初始化函数中所执行的registerNativeMethods()方法则为Java层访问JNI层打通了道路。换句话说,Binder初始化的工作就是通过JNI建立起Native Binder与Java Binder之间互相通信的桥梁。

下面通过一个例子来分析Java Binder的工作流程。2.2.3 窥一斑,可见全豹乎

这个例子源自ActivityManagerService,我们试图通过它揭示Java层Binder的工作原理。先来描述一下该例子的分析步骤: 首先分析AMS如何将自己注册到ServiceManager。 然后分析AMS如何响应客户端的Binder调用请求。

本例的起点是setSystemProcess,其代码如下所示:

上面所示代码行的目的是将ActivityManagerService服务(以后简称AMS)加到ServiceManager中。

在整个Android系统中有一个Native的ServiceManager(以后简称SM)进程,它统筹管理Android系统上的所有服务。成为一个服务的首要条件是先在SM中注册。下面来看Java层的服务是如何向SM注册的。

1.向ServiceManager注册服务(1)创建ServiceManagerProxy

向SM注册服务的函数叫addService,其代码如下:

首先需要搞清楚getIServiceManager()方法返回的是一个什么对象?参考其实现:

asInterface()方法的参数为BinderInternal.getContextObject()的返回值。于是这个简短的方法中有两个内容值得讨论:BinderInternal.getContextObject()以及asInterface()。

可见,Java层的ServiceManager需要在Native层获取指向Native进程中ServiceManager的BpProxy。这个BpProxy不能由Java层的ServiceManager直接使用,于是android_os_BinderInteral_getContextObject()函数通过javaObjectForIBinder()函数将创建一个封装了这个BpProxy的一个Java对象并返回给调用者。ServiceManager便可以通过这个Java对象实现对BpProxy的访问。参考这个Java对象的创建过程:

BinderInternal.getContextObject的代码有点多,简单整理一下,可知该函数完成了以下两个工作: 创建了一个Java层的BinderProxy对象。 通过JNI,该BinderProxy对象和一个Native的BpProxy对象挂钩,

而该BpProxy对象的通信目标就是ServiceManager。

接下来讨论asInterface()方法,大家还记得在Native层Binder中那个著名的interface_cast宏吗?在Java层中,虽然没有这样的宏,但是定义了一个类似的函数asInterface。下面来分析ServiceManagerNative类的asInterface函数,其代码如下:

上面代码和Native层interface_cast宏非常类似,都是以一个BpProxy对象为参数构造一个和业务相关的Proxy对象,例如这里的ServiceManagerProxy对象。ServiceManagerProxy对象的各个业务函数会将相应请求打包后交给BpProxy对象,最终由BpProxy对象发送给Binder驱动以完成一次通信。说明实际上BpProxy也不会直接和Binder驱动交互,真正和Binder驱动交互的是IPCThreadState。(2)addService函数分析

现在来分析ServiceManagerProxy的addService函数,其代码如下:

BinderProxy的transact是一个native函数,其实现函数的代码如下所示:

看了上面的代码你会发现,Java层的Binder最终还是要借助Native的Binder进行通信的。说明从架构的角度看,在Java中搭建了一整套框架,如IBinder接口、Binder类和BinderProxy类。但是从通信角度看,不论架构的编写采用的是Native语言还是Java语言,只要把请求传递到Binder驱动就可以了,所以通信的目的是向binder发送请求和接收回复。在这个目的之上,考虑到软件的灵活性和可扩展性,于是编写了一个架构。反过来说,也可以不使用架构(即没有使用任何接口、派生之类的东西)而直接和binder交互,例如ServiceManager作为Binder的一个核心程序,就是直接读取/dev/binder设备,获取并处理请求。从这一点上看,Binder的目的虽然简单(即打开binder设备,然后读请求和写回复),但是架构复杂(编写各种接口类和封装类等)。我们在研究源码时,一定要先搞清楚目的。实现只不过是达到该目的的一种手段和方式。脱离目的的实现,如缘木求鱼,很容易偏离事物本质。

在对addService进行分析时曾提示writeStrongBinder是一个特别的函数。那么它特别在哪里呢?下面将给出解释。(3)三人行之Binder、JavaBBinderHolder和JavaBBinder

ActivityManagerService从ActivityManagerNative类派生,并实现了一些接口,其中和Binder相关的只有这个ActivityManagerNative类,其原型如下:

ActivityManagerNative从Binder派生,并实现了IActivityManager接口。下面来看ActivityManagerNative的构造函数:

而ActivityManagerNative父类的构造函数则是Binder的构造函数:

Binder构造函数会调用native的init函数,其实现的代码如下:

从上面代码可知,Java的Binder对象将和一个Native的JavaBBinderHolder对象相关联。那么,JavaBBinderHolder是何方神圣呢?其定义如下:

从派生关系上可以发现,JavaBBinderHolder仅从RefBase派生,所以它不属于Binder家族。Java层的Binder对象为什么会和Native层的一个与Binder家族无关的对象绑定呢?仔细观察JavaBBinderHolder的定义可知:JavaBBinderHolder类的get函数中创建了一个JavaBBinder对象,这个对象就是从BnBinder派生的。

那么,这个get函数是在哪里调用的?答案在下面这句代码中:

writeStrongBinder会做一个替换工作,下面是它的native代码实现:

根据上面的介绍会发现,addService实际添加到Parcel的并不是AMS本身,而是一个叫JavaBBinder的对象。而最终传递到Binder驱动的正是这个JavaBBinder对象。

读者此时容易想到,Java层中所有的Binder对应的都是这个JavaBBinder。当然,不同的Binder对象对应不同的JavaBBinder对象。

图2-2展示了Java Binder、JavaBBinderHolder和JavaBBinder的关系。图2-2Java Binder、JavaBBinderHolder和JavaBBinder三者的关系

从图2-2可知: Java层的Binder通过mObject指向一个Native层的

JavaBBinderHolder对象。 Native层的JavaBBinderHolder对象通过mBinder成员变量指向一

个Native的JavaBBinder对象。 Native的JavaBBinder对象又通过mObject变量指向一个Java层的

Binder对象。

为什么不直接让Java层的Binder对象指向Native层的JavaBBinder对象呢?由于缺乏设计文档,这里不便妄加揣测,但从JavaBBinderHolder的实现上来分析,估计和垃圾回收(内存管理)有关,因为JavaBBinderHolder中的mBinder对象的类型被定义成弱引用wp了。建议对此有更好的解释的读者,不妨与大家分享一下。

2.ActivityManagerService响应请求

初见JavaBBinde时,多少有些吃惊。回想一下Native层的Binder架构:虽然在代码中调用的是Binder类提供的接口,但其对象却是一个实际的服务端对象,例如MediaPlayerService对象、AudioFlinger对象。

而在Java层的Binder架构中,JavaBBinder却是一个和业务完全无关的对象。那么,这个对象如何实现不同业务呢?

为回答此问题,我们必须查看它的onTransact函数。当收到请求时,系统会调用这个函数。说明关于这个问题,建议读者阅读卷I第6章“深入理解Binder”。

就本例而言,上面代码中的mObject就是ActivityManagerService,现在调用它的exec-Transact()方法,该方法在Binder类中实现,具体代码如下:

ActivityManagerNative类实现了onTransact函数,代码如下:

由此可以看出,JavaBBinder仅是一个传声筒,它本身不实现任何业务函数,其工作是: 当它收到请求时,只是简单地调用它所绑定的Java层Binder对象

的exeTransact。 该Binder对象的exeTransact调用其子类实现的onTransact函数。 子类的onTransact函数将业务又派发给其子类来完成。请读者务

必注意其中的多层继承关系。

通过这种方式,来自客户端的请求就能传递到正确的Java Binder对象了。图2-3展示AMS响应请求的整个流程。

在图2-3中,右上角的大方框表示AMS对象,其中的虚线箭头表示调用子类重载的函数。图2-3AMS响应请求的流程2.2.4 理解AIDL

经过上一节的介绍,你已经明白在Java层Binder的架构中,Bp端可以通过BinderProxy的transact()方法与Bn端发送请求,而Bn端通过继承Binder类重写onTransact()接收并处理来自Bp端的请求。这个结构非常清晰而且简单,但是实现起来却颇为烦琐。于是Android提供了AIDL语言以及AIDL解释器自动生成一个服务的Bn端,即Bp端用于处理Binder通信的代码。

AIDL的语法与定义一个Java接口的语法非常相似。为了避免业务实现对分析的干扰,本节通过一个最简单的例子对AIDL的原理进行介绍。

IMyServer.aidl定义了一个名为IMyServer的Binder服务,并提供了一个可以跨Binder调用的接口foo()。可以通过aidl工具将其解析为一个实现了Bn端及Bp端通过Binder进行通信的Java源代码。具体命令如下:

生成的IMyServer.java可以在com/understanding/samples/文件夹下找到。建议读者可以阅读aidl有关的文档了解此工具的详细功能。

可见一个AIDL文件被aidl工具解析之后会有三个产物: IMyServer接口。它仅仅用来在Java中声明IMyServer.aidl中所声

明的接口。 IMyServer.Stub类。这个继承自Binder类的抽象类实现了Bn端与

Binder通信相关的代码。 IMyServer.Stub.Proxy类。这个类实现了Bp端与Binder通信相关

的代码。

在完成aidl的解析之后,为了实现一个Bn端,开发者需要继承IMyServer.Stub类并实现其抽象方法。如下所示:

于是每一个MyServer类的实例,都具有了作为Bn端的能力。典型的做法是将MyServer类的实例通过ServiceManager.addService()将其注册为一个系统服务,或者在一个Android标准Service的onBind()方法中将其作为返回值使之可以被其他进程访问。另外,也可以通过Binder调用将其传递给另外一个进程,使之成为一个跨进程的回调对象。

那么Bp端将如何使用IMyServer.Proxy呢?在Bp端所在进程中,一旦获取了IMyServer的BinderProxy(通过ServiceManager.getService()、onServiceConnected()或者其他方式),就可以通过如下方式获得一个IMyServer.Proxy:

IMyServer.Stub.asInterface()的实现如下:

可见,AIDL使得构建一个Binder服务的工作大大简化了。2.2.5 Java层Binder架构总结

图2-4展示了Java层的Binder架构。

根据图2-4可知: 对代表客户端的BinderProxy来说,Java层的BinderProxy在Native

层对应一个BpBinder对象。凡是从Java层发出的请求,首先从

Java层的BinderProxy传递到Native层的BpBinder,继而由

BpBinder将请求发送到Binder驱动。 对代表服务端的Service来说,Java层的Binder在Native层有一个

JavaBBinder对象。前面介绍过,所有Java层的Binder在Native层

都对应为JavaBBinder,而JavaBBinder仅起到中转作用,即把来

自客户端的请求从Native层传递到Java层。 系统中依然只有一个Native的ServiceManager。图2-4Java层Binder架构

至此,Java层的Binder架构已介绍完毕。从前面的分析可以看出,Java层的Binder非常依赖Native层的Binder。建议想进一步了解Binder的读者要深入了解这一问题,有必要阅读卷I的第6章。2.3 心系两界的MessageQueue

卷I第5章介绍过,MessageQueue类封装了与消息队列有关的操作。在一个以消息驱动的系统中,最重要的两部分就是消息队列和消息处理循环。在Andrid 2.3以前,只有Java世界的居民有资格向MessageQueue中添加消息以驱动Java世界的正常运转,但从Android 2.3开始,MessageQueue的核心部分下移至Native层,让Native世界的居民也能利用消息循环来处理他们所在世界的事情。因此现在的MessageQueue心系Native和Java两界。2.3.1 MessageQueue的创建

现在来分析MessageQueue是如何跨界工作的,其代码如下:

nativeInit()方法的真正实现为android_os_MessageQueue_nativeInit()函数,其代码如下:

nativeInit函数在Native层创建了一个与MessageQueue对应的NativeMessageQueue对象,其构造函数如下:

Native的Looper是Native世界中参与消息循环的一位重要角色。虽然它的类名和Java层的Looper类一样,但此二者其实并无任何关系。这一点以后还将详细分析。2.3.2 提取消息

当一切准备就绪后,Java层的消息循环处理,也就是Looper会在一个循环中提取并处理消息。消息的提取就是调用MessageQueue的next()方法。当消息队列为空时,next就会阻塞。MessageQueue同时支持Java层和Native层的事件,那么其next()方法该怎么实现呢?具体代码如下:

看到这里,可能会有人觉得这个MessageQueue很简单,不就是从以前在Java层的wait变成现在Native层的wait了吗?但是事情本质比表象要复杂得多。请思考下面的情况:

在nativePollOnce()返回后,next()方法将从mMessages中提取一个消息。也就是说,要让nativePollOnce()返回,至少要添加一个消息到消息队列,否则nativePollOnce()不过是做了一次无用功罢了。

如果nativePollOnce()将在Native层等待,就表明Native层也可以投递Message,但是从Message类的实现代码上看,该类和Native层没有建立任何关系。那么nativePollOnce()在等待什么呢?

对于上面的问题,相信有些读者心中已有了答案:nativePollOnce()不仅在等待Java层来的Message,实际上在Native层还做了大量工作。

下面我们来分析Java层投递Message并触发nativePollOnce工作的正常流程。

1.在Java层投递Message

MessageQueue的enqueueMessage函数完成将一个Message投递到MessageQueue中的工作,其代码如下:

上面的代码比较简单,主要功能是: 将message按执行时间排序,并加入消息队列。 根据情况调用nativeWake函数,以触发nativePollOnce函数,结

束等待。建议虽然代码简单,但是对于那些不熟悉多线程的读者,还是要细细品味一下mBlocked值的作用。我们常说细节体现美,代码也一样,这个小小的mBlocked正是如此。

2.nativeWake函数分析

nativeWake函数的代码如下所示:

Wake()函数则更为简单,仅仅向管道的写端写入一个字符“W”,这样管道的读端就会因为有数据可读而从等待状态中醒来。2.3.3 nativePollOnce函数分析

nativePollOnce()的实现函数是android_os_MessageQueue_nativePollOnce,代码如下:

分析pollOnce函数:

Looper的pollOnce函数如下:

上面的函数将调用另外一个有4个参数的pollOnce函数,这个函数的原型如下:

其中: timeOutMillis参数为超时等待时间。如果为-1,则表示无限等

待,直到有事件发生为止。如果值为0,则无须等待立即返回。 outFd用来存储发生事件的那个文件描述符。 outEvents用来存储在该文件描述符1上发生了哪些事件,目前支

持可读、可写、错误和中断4个事件。这4个事件其实是从epoll

事件转化而来。后面我们会介绍大名鼎鼎的epoll。 outData用于存储上下文数据,这个上下文数据是由用户在添加

监听句柄时传递的,它的作用和pthread_create函数最后一个参

数param一样,用来传递用户自定义的数据。

另外,pollOnce函数的返回值也具有特殊的意义,具体如下: 当返回值为ALOOPER_POLL_WAKE时,表示这次返回是由

wake函数触发的,也就是管道写端的那次写事件触发的。 返回值为ALOOPER_POLL_TIMEOUT表示等待超时。 返回值为ALOOPER_POLL_ERROR,表示等待过程中发生错

误。 返回值为ALOOPER_POLL_CALLBACK,表示某个被监听的句柄

因某种原因被触发。

这时,outFd参数用于存储发生事件的文件句柄,outEvents用于存储所发生的事件。

上面这些知识是和epoll息息相关的。提示查看Looper的代码会发现,Looper采用了编译选项(即#if和#else)来控制是否使用epoll作为I/O复用的控制中枢。鉴于现在大多数系统都支持epoll,这里仅讨论使用epoll的情况。

1.epoll基础知识介绍

epoll机制提供了Linux平台上最高效的I/O复用机制,因此有必要介绍一下它的基础知识。

从调用方法上看,epoll的用法和select/poll非常类似,其主要作用就是I/O复用,即在一个地方等待多个文件句柄的I/O事件。

下面通过一个简单例子来分析epoll的工作流程。

epoll整体使用流程如上面代码所示,基本和select/poll类似,不过作为Linux平台最高效的I/O复用机制,这里有些内容供读者参考。

epoll的效率为什么会比select高?其中一个原因是调用方法。每次调用select时,都需要把感兴趣的事件复制到内核中,而epoll只在epll_ctl进行加入的时候复制一次。另外, epoll内部用于保存事件的数据结构使用的是红黑树,查找速度很快。而select采用数组保存信息,不但一次能等待的句柄个数有限,并且查找起来速度很慢。当然,在只等待少量文件句柄时,select和epoll效率相差不是很多,但还是推荐使用epoll。

epoll等待的事件有两种触发条件,一个是水平触发(EPOLLLEVEL),另外一个是边缘触发(EPOLLET,ET为Edge Trigger之意),这两种触发条件的区别非常重要。读者可通过man epoll查阅系统提供的更为详细的epoll机制。

最后,关于pipe,还想提出一个小问题供读者思考讨论:

为什么Android中使用pipe作为线程间通信的方式?对于pipe的写端写入的数据,读端都不感兴趣,只是为了简单唤醒。POSIX不是也有线程间同步函数吗?为什么要用pipe呢?

关于这个问题的答案,可参见邓凡平的一篇博文《随笔之如何实现一个线程池》。网址为http://www.cnblogs.com/innost/archive/2011/11/24/2261454.html。

2.pollOnce()函数分析

下面分析带4个参数的pollOnce()函数,代码如下:

初看上面的代码,可能会让人有些丈二和尚摸不着头脑。但是把pollInner()函数分析完毕,大家就会明白很多。pollInner()函数非常长,把用于调试和统计的代码去掉,结果如下:

看完代码了,是否还有点模糊?那么,回顾一下pollInner函数的几个关键点: 首先需要计算一下真正需要等待的时间。 调用epoll_wait函数等待。 epoll_wait函数返回,这时候可能有三种情况:○发生错误,则跳转到Done处。○超时,这时候也跳转到Done处。○epoll_wait监测到某些文件句柄上有事件发生。 假设epoll_wait因为文件句柄有事件而返回,此时需要根据文件

句柄来分别处理:○如果是管道读端有事件,则认为是控制命令,可以直接读取管道中的数据。○如果是其他fd发生事件,则根据Request构造Response,并push到Response数组中。 真正开始处理事件是在有Done标志的位置。○首先处理Native的Message。调用Native Handler的handleMessage处理该Message。○处理Response数组中那些带有callback的事件。

上面的处理流程还是比较清晰的,但还是有一个拦路虎,那就是mRequests,下面就来清剿这个拦路虎。

3.添加监控请求

添加监控请求其实就是调用epoll_ctl增加文件句柄。下面通过从Native的Activity找到的一个例子来分析mRequests。

Looper的addFd()代码如下所示:

4.处理监控请求

我们发现在pollInner()函数中,当某个监控fd上发生事件后,就会把对应的Request取出来调用。

根据前面的知识可知,并不是单独处理Request,而是需要先收集Request,等到Native Message消息处理完之后再做处理。这表明,在处理逻辑上,Native Message的优先级高于监控fd的优先级。

下面来了解如何添加Native的Message。

5.Native的sendMessage

Android 2.2中只有Java层才可以通过sendMessage()往MessageQueue中添加消息,从4.0开始,Native层也支持sendMessage()。sendMessage()的代码如下:2.3.4 MessageQueue总结

想不到,一个小小的MessageQueue竟然有如此多的内容。在后面分析Android输入系统时,会再次在Native层和MessageQueue碰面,这里仅是为后面的相会打下一定的基础。

现在将站在一个比具体代码更高的层次来认识一下MessageQueue及其伙伴。

1.消息处理的大家族合照

MessageQueue只是消息处理大家族的一员,该家族的成员合照如图2-5所示。

结合前述内容可从图2-5中得到:图2-5 消息处理的家族合照 Java层提供了Looper类和MessageQueue类,其中Looper类提供

循环处理消息的机制,MessageQueue类提供一个消息队列,以

及插入、删除和提取消息的函数接口。另外,Handler也是在

Java层常用的与消息处理相关的类。 MessageQueue内部通过mPtr变量保存一个Native层的

NativeMessageQueue对象, mMessages保存来自Java层的

Message消息。 NativeMessageQueue保存一个Native层的Looper对象,该Looper

从ALooper派生,提供pollOnce和addFd等函数。 Java层有Message类和Handler类,而Native层对应也有Message

类和MessageHandler抽象类。在编码时,一般使用的是

MessageHandler的派生类WeakMessageHandler。注意在include/media/stagfright/foundation目录下也定义了一个ALooper类,它是供stagefright使用的类似Java消息循环的一套基础类。这种同名类的产生,估计是两个事先未做交流的团队的人编写的。

2.MessageQueue处理流程总结

MessageQueue核心逻辑下移到Native层后,极大地拓展了消息处理的范围,总结后有以下几点: MessageQueue继续支持来自Java层的Message消息,也就是早

期的Message加Handler的处理方式。 MessageQueue在Native层的代表NativeMessageQueue支持来自

Native层的Message,是通过Native层的Message和

MessageHandler来处理的。 NativeMessageQueue还处理通过addFd添加的Request。在后面

分析输入系统时,还会大量碰到这种方式。 从处理逻辑上看,先是Native的Message,然后是Native的

Request,最后才是Java的Message。2.4 本章小结

本章先对Java层的Binder架构做了一次较为深入的分析。Java层的Binder架构和Native层Binder架构类似,但是Java的Binder在通信上还是依赖Native层的Binder。建议想进一步了解Native Binder工作原理的读者,阅读卷I第6章。另外,本章还对MessageQueue进行了较为深入的分析。Android 2.2中那个功能简单的MessageQueue现在变得复杂了,原因是该类的核心逻辑下移到Native层,导致现在的MessageQueue除了支持Java层的Message派发外,还新增了支持Native Message派发以及处理来自所监控的文件句柄的事件。第3章 深入理解AudioService

本章主要内容: 探讨AudioService如何进行音量管理 了解音频外设的管理机制 探讨AudioFocus的工作原理

本章涉及的源代码文件名及位置: AudioManager.java

framework/base/media/java/android/media/AudioManager.java AudioService.java

framework/base/media/java/android/media/AudioService.java AudioSystem.java

framework/base/media/java/android/media/AudioSystem.java VolumePanel.java

framework/base/core/java/android/view/VolumePanel.java WiredAccessoryObserver.java

framework/base/services/java/com/android/server/WiredAccessoryObserver.java PhoneWindow.java

framework/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java Activity.java

framework/base/core/java/android/app/Activity.java3.1 概述

通过对卷I第7章的学习,相信大家已经对AudioTrack、AudioRecord、音频设备路由等知识有了深入了解。这一章将详细介绍音频系统在Java层的实现,围绕AudioService这个系统服务深入探讨在Android SDK中看到的音频相关的机制的实现。

在分析Android音频系统时,习惯将其实现分为两个部分:数据流和策略。数据流描述了音频数据从数据源流向目的地的过程。而策略则是管理及控制数据流的路径与呈现的过程。在卷I所探讨的Native层音频系统中,AudioTrack、AudioRecord和AudioFlinger可以划归到数据流的范畴去讨论,而AudioPolicy相关的内容则属于策略范畴。

音频系统在Java层中基本上是不参与数据流的。虽然有AudioTrack和AudioRecord这两个类,但是它们只是Native层同名类的Java封装。抛开这两个类,AudioService这个系统服务包含或使用了几乎所有与音频相关的内容,所以说AudioService是一个音频系统的大本营,它的功能非常多,而且它们之间的耦合性也不大。本章将从三个方面来探讨AudioService。 音量管理。

从按下音量键到弹出音量调节提示框的过程,以及静音功能的工作原理。 音频IO设备的管理。

我们将详细探讨从插入耳机到声音经由耳机发出这个过程中,AudioService的工作内容。 AudioFocus机制。

AudioService在Android 2.3及以后版本中提供了AudioFocus机制,用以结束多个音频应用混乱的交互现状。音频应用在播放音频的过程中需要合理地申请与释放AudioFocus,并且根据AudioFocus所有权的变化来调整自己的播放行为。我们将从音频应用开始播放音频,到播放完成的过程中探讨AudioFocus的作用及原理。

AudioService的类图如图3-1所示。图3-1AudioService的类图

由图3-1可知: AudioService继承自IAudioService.Stub。IAudioService.Stub类是

通过IAudioService. aidl自动生成的。AudioService位于Bn端。 AudioManager拥有AudioService的Bp端,是AudioService在客户

端的一个代理。几乎所有客户端对AudioManager进行的请求,

最终都会交由AudioService实现。 AudioService的功能实现依赖AudioSystem类。AudioSystem无法

实例化,它是Java层到native层的代理。AudioService将通过它

与AudioPolicyService及AudioFlinger进行交互。

下面开始我们的AudioService之旅吧。3.2 音量管理

Android手机有两种改变系统音量的方式。最直接的做法就是通过手机的音量键进行音量调整,还有一种做法是从设置界面中调整某一种类型音频的音量。另外,应用程序可以随时将某种类型的音频静音。它们都是通过AudioService进行的。

本节将从上述三个方面对AudioService的音量管理进行探讨。3.2.1 音量键的处理流程

1.触发音量键

在音量键被按下后,Android输入系统将该事件一路派发给Activity,如果无人截获并处理这个事件,承载当前Activity的显示PhoneWindow类的onKeyDown()或onKeyUp()函数将会处理,从而开始通过音量键调整音量的处理流程。输入事件的派发机制及PhoneWindow类的作用将在后续章节中详细介绍,现在只需要知道,PhoneWindow描述了一片显示区域,用于显示与管理我们所看到的Activity和对话框等内容。同时,它还是输入事件的派发对象,而且只有显示在最上面的PhoneWindow才会收到事件。注意按照Android的输入事件派发策略,Window对象在事件的派发队列中位于Activity的后面,所以应用程序可以重写自己的Activity.onKeyDown()函数以截获音量键的消息,将其用作其他的功能。比如说,在一个相机应用中,按下音量键所执行的动作是拍照而不是调节音量。

注意handleKeyDown()函数的第二个参数,它的意义是指定音量键将要改变哪一种流类型的音量。在Android中,音量的控制与流类型是密不可分的,每种流类型都独立地拥有自己的音量设置,它们在绝大部分情况下互不干扰,例如音乐音量、通话音量就是相互独立的。所以说,离开流类型谈音量是没有意义的。在Android中,音量这个概念描述的一定是某一种流类型的音量。

这里传入了mVolumeControlStreamType,那么这个变量的值是从哪里来的呢?Activity类中有一个函数名为setVolumeControlStream(int streamType)。应用可以通过调用这个函数来指定显示这个Activity时音量键所控制的流类型。这个函数的内容很简单,就一行,如下:

getWindow()的返回值就是用于显示当前Activity的PhoneWindow。从名字就可以看出,这个调用改变了mVolumeControlStreamType,于是也改变了按下音量键后传入AudioManager. handleKeyUp()函数的参数,从而达到setVolumeControlStream的目的。同时,还应该能看出,这个设置被绑定到Activity的Window上,在不同Activity之间切换时,接收按键事件的Window也会随之切换,所以应用不需要去考虑在其生命周期中音量键所控制的流类型的切换问题。

AudioManager的handleKeyDown()的实现很简单,在一个switch中,它调用了AudioService的adjustSuggestedStreamVolume(),所以直接看一下AudioService的这个函数。

2.adjustSuggestedStreamVolume()分析

我们先来看函数原型:

adjustSuggestedStreamVolume()有三个参数,第一个参数direction指示了音量的调整方向,1为增大,-1为减小;第二个参数suggestedStreamType表示要求调整音量;第三个参数flags的意思就不那么容易理解了。其实AudioManager在handleKeyDown()中设置了两个flag,分别是FLAG_SHOW_UI和FLAG_VIBRATE。从名字上我们就能看出一些端倪。前者告诉AudioService我们需要弹出一个音量控制面板。而在handleKeyUp()里设置了FLAG_PLAY_SOUND,这是为什么当在松开音量键后“有时候”会有一个提示音。注意, handleKeyUp()中设置了FLAG_PLAY_SOUND,但只是有时候这个flag才会生效,在下面的代码中可以看到这是为什么。还需要注意的是,第二个参数名为suggestedStreamType,从其命名来推断,这个参数传入的流类型对AudioService来说只是一个建议,是否采纳这个建议, AudioService有自己的考虑。

看一下它的实现:注意初看这段代码时,可能有读者对下面这句代码感到疑惑:其实,这是为了满足所谓的“将铃声音量用作通知音量”这种需求。这就需要实现在两个有这个需求的流A与B之间建立起一个A→B映射。当我们对A流进行音量操作时,实际上是在操作B流。笔者个人认为这个功能对用户体验的提升并不大,却给AudioService的实现增加了不小的复杂度。直观上来想,我们可以使用一个HashMap解决这个问题,键是源流类型,值是目标流类型。而Android使用了一个更简单却不是那么好理解的方法来完成这件事。AudioService用一个名为mStreamVolumeAlias的整型数组来描述这个映射关系。要实现“以铃声音量用作音乐音量”,只需要修改相应位置的值为STREAM_RING即可,就像下面这样:mStreamVolumeAlias[AudioSystem.STREAM_MUSIC]=AudioSystem.STREAM_RING;之后,因为需要对A流进行音量操作时,实际上是在操作B流,所以就不难理解为什么在很多和流相关的函数里都会先做这样的一个转换:streamType=mStreamVolumeAlias[streamType];其具体的工作方式就留给读者思考。在本章的分析过程中,大可忽略这种转换,这并不影响我们对音量控制原理的理解。

简单来说,这个函数做了三件事: 确定要调整音量的流类型。 在某些情况下屏蔽FLAG_PLAY_SOUND。 调用adjustStreamVolume()。

关于这个函数有几点仍需要说明一下。在函数刚开始的时候有一个判断,条件是一个名为mVolumeControlStream的整型变量是否等于-1,从这块代码来看,mVolumeControlStream比参数传入的suggestedStreamType厉害多了,只要它不是-1,要调整音量的流类型就是它。那这么厉害的控制手段的作用是什么?其实,mVolumeControlStream是VolumePanel通过forceVolumeControlStream()函数设置的。什么是VolumePanel呢?就是我们按下音量键后的那个音量调节通知框。VolumePanel在显示时会调用forceVolumeControlStream强制后续的音量键操作固定为促使它显示的那个流类型,并在它关闭时取消这个强制设置,即设置mVolumeControlStream为-1。这个在后面分析VolumePanel时会看到。

接下来我们继续看一下adjustStreamVolume()的实现。

3.adjustStreamVolume()分析

[AudioService.java-->AudioService.adjustStreamVolume()]

在这个函数的实现中,有一个非常重要的类型:VolumeStreamState。前面提到过,Android的音量是依赖于某种流类型的。如果Android定义了N个流类型,AudioService就需要维护N个音量值与之对应。另外每个流类型的音量等级范围不一样,所以还需要为每个流类型维护它们的音量调节范围。VolumeStreamState类的功能就是为了保存与一个流类型所有音量相关的信息。AudioService为每一种流类型都分配了一个VolumeStreamState对象,并且以流类型的值为索引,保存在一个名为mStreamStates的数组中。在这个函数中调用了VolumeStreamState对象的adjustIndex()函数,于是就改变了这个对象中存储的音量值。不过,仅仅是改变了它的存储值,并且没有把这个变化设置到底层。

总结一下这个函数都做了什么。 准备工作。计算按下音量键的音量步进值。细心的读者一定注意

到了,这个步进值是10而不是1。原来,在VolumeStreamState

中保存的音量值是其实际值的10倍。为什么这么做呢?这是为

了在不同流类型之间进行音量转换时能够保证一定精度的一种实

现,其转换过程读者可以参考rescaleIndex()函数的实现。我们可

以将这种做法理解为在转换过程中保留了小数点后一位的精度。

其实,直接使用float类型来保存岂不更简单? 检查是否需要改变情景模式。checkForRingerModeChange()和情

景模式有关。读者可以自行研究其实现。 调用adjustIndex()更改VolumeStreamState对象中保存的音量值。 通过sendMsg()发送消息MSG_SET_DEVICE_VOLUME到

mAudioHandler。 调用sendVolumeUpdate()函数,通知外界音量发生了变化。

我们将重点分析后面三项内容:adjustIndex()、MSG_SET_DEVICE_VOLUME消息的处理和sendVolumeUpdate()。

4.VolumeStreamState的adjustIndex()分析

我们先看一下这个函数的定义:

这个函数很简单,下面再看一下setIndex()的实现:

在这个函数中有三项工作要做: 首先保存设置的音量值。这是VolumeStreamState的本职工作,

这和Android 4.1之前的版本不一样,音量值与设备相关联了。因

此对同一种流类型来说,在不同的音频设备下将会拥有不同的音

量值。 然后根据参数的要求保存音量值到mLastAudibleIndex中。从名

字就可以看出,它保存了静音前的音量。当取消静音时,

AudioService就会恢复到这里保存的音量。 再就是对流映射的处理。既然A→B,那么在设置B的音量的同时

要改变A的音量。这就是后面那个循环的作用。

可以看出,VolumeStreamState.adjustIndex()除了更新自己所保存的音量值外,没有做其他的事情。接下来再看一下MSG_SET_DEVICE_VOLUME的消息处理做了什么。

5.MSG_SET_DEVICE_VOLUME消息的处理

adjustStreamVolume()函数使用sendMsg()函数发送MSG_SET_DEVICE_VOLUME消息给mAudioHandler,这个Handler运行在AudioService的主线程上。直接看一下在mAudio-Handler中负责处理MSG_SET_DEVICE_VOLUME消息的setDeviceVolume()函数:注意sendMsg()是一个异步操作,这就意味着,完成adjustIndex()更新音量信息后adjustStreamVolume()函数就返回了,但是音量并没有立刻被设置到底层。不过由于Handler处理多个消息的过程是串行的,这就隐含着一种风险:如果当Handler正在处理某一个消息时发生了阻塞,那么按下音量键,虽然调用adjustStreamVolume()可以立刻返回,并且从界面上看或用getStreamVolume()获取音量值都是没有问题的,但是手机发出声音时的音量大小并没有改变。

6.sendVolumeUpdate()分析

接下来,分析一下sendVolumeUpdate()函数,它用于通知外界音量发生了变化。

这个函数将音量的变化通过广播的形式通知给其他感兴趣的模块。同时,它还特别通知了mVolumePanel。mVolumePanel是VolumePanel类的一个实例。我们所看到的音量调节通知框就是它。

至此,从按下音量键开始的整个处理流程就完结了。在继续分析音量调节通知框的工作原理之前,先对之前的分析过程进行总结,参考图3-2的序列图。图3-2 通过音量键调整音量的处理流程

结合上面分析的结果,由图3-2可知: 音量键处理流程的发起者是PhoneWindow。 AudioManager仅仅起到代理的作用。 AudioService接受AudioManager的调用请求,操作

VolumeStreamState的实例进行音量的设置。 VolumeStreamState负责保存音量设置,并且提供了将音量设置

到底层的方法。 AudioService负责将设置结果以广播的形式通知外界。

到这里,相信大家对音量调节的流程已经有了一个比较清晰的认识。接下来我们将介绍音量调节通知框的工作原理。

7.音量调节通知框的工作原理

在分析sendVolumeUpdate()函数时曾经注意到,它调用了mVolumePanel的post-VolumeChanged()函数。mVolumePanel是一个VolumePanel的实例,作为一个Handler的子类,它承接了音量变化的UI/声音的通知工作。在继续上面的讨论之前,先了解一下VolumePanel工作的基本原理。

VolumePanel位于android.view包下,却没有在API中提供,因为它只能被AudioService使用,所以和AudioService放在一个包下可能更合理一些。从这个类的注释上可以看到,谷歌的开发人员对它被放在android.view下也有极大不满(What AMass!他们这么写道……)。

VolumePanel下定义了两个重要的子类型,分别是StreamResources和StreamControl。StreamResources实际上是一个枚举,它的每一个可用元素保存了一个流类型的通知框所需要的各种资源,如图标、提示文字等。StreamResources的定义就像下面这样:

这几个枚举项组成了一个名为STREAM的数组,如下:

VolumePanel将从这个STREAMS数组中获取它所支持的流类型的相关资源。这么做是不是有点啰嗦呢?事实上,在这里使用枚举并没有什么特殊的意义,使用一个普通的Java类来定义StreamResources就已经足够了。

StreamControl类则保存了一个流类型的通知框所需要显示的控件,其定义如下:

很简单对不对?StreamControl实例中保存了音量调节通知框中所需的所有控件。关于这个类在VolumePanel的使用,我们可能很直观地认为只有一个StreamControl实例,在对话框显示时,使其保存的控件按需加载指定流类型的StreamResources实例中定义的资源。其实不然,出于对运行效率的考虑,StreamControl实例也是每个流类型人手一份,和StreamResources实例形成一一对应的关系。所有的StreamControl实例被保存在一个以流类型的值为键的Hashtable中,名为mStreamControls。我们可以在StreamControl的初始化函数createSliders()中一窥端倪。

值得一提的是,这个初始化的工作并没有在构造函数中进行,而是在postVolume-Changed()函数中处理的。

既然已经有了通知框所需要的资源和通知框的控件,接下来就要有一个对话框承载它们。没错,VolumePanel保存了一个名为mDialog的Dialog实例,这就是通知框的本身了。每当有新的音量变化到来时,mDialog的内容就会被替换为指定流类型对应的StreamControl中所保存的控件,并且根据音量变化情况设置其音量条的位置,最后调用mDialog.show()显示出来。同时,发送一个延时消息MSG_TIMEOUT,这条延时消息生效时,将会关闭提示框。

StreamResource、StreamControl与mDialog的关系就像图3-3所示的那样,StreamControl可以说是mDialog的配件,随需拆卸。图3-3StreamResource、StreamControl与mDialog的关系

接下来具体看一下VolumePanel在收到音量变化通知后都做了什么。我们在上一小节中说到了mVolumePanel.postVolumeChanged()函数。它的内容很简单,直接发送了一条消息MSG_VOLUME_CHANGED,然后在handleMessage中调用onVolumeChanged()函数进行真正的处理。注意VolumePanel在MSG_VOLUME_CHANGED的消息处理函数中调用onVolume-Changed()函数,而不是直接在postVolumeChanged()函数中直接调用。这么做是有实际意义的。由于Android要求只能在创建控件的线程中对控件进行操作。postVolumeChanged()作为一个回调性质的函数,不能要求调用者位于哪个线程中。所以必须通过向Handler发送消息的方式,将后续的操作转移到指定的线程中。在设计具有UIController功能的类时,VolumePanel的实现方式有很好的参考意义。

下面看一下onVolumeChanged()函数的实现:

注意最后一个resetTimeout()的调用,其实它重新延时发送了MSG_TIMEOUT消息。当MSG_TIMEOUT消息生效时,mDialog将被关闭。

之后就是onShowVolumeChanged了。这个函数负责为通知框的内容填充音量、图表等信息,然后再显示通知框(如果还没有显示)。以铃声音量为例,省略其他的代码。

至此,音量调节通知框就被显示出来了,下面总结一下它的工作过程: postVolumeChanged()是VolumePanel显示的入口。 检查flags中是否有FLAG_SHOW_UI。 VolumePanel会在第一次被要求弹出时初始化其控件资源。 mDialog加载指定流类型对应的StreamControl,也就是控件。 显示对话框并开始超时计时。 超时计时到达,关闭对话框。

到此为止,AudioService对音量键的处理流程介绍完毕。而Android还有另外一种改变音量的方式,即音量设置函数etStreamVolume(),下面对其进行介绍。3.2.2 通用的音量设置函数setStreamVolume()

除了可以通过音量键调节音量以外,用户还可以在系统设置中进行调节。Audio-Manager.setStreamVolume()是系统设置界面中调整音量所使用的接口。

1.setStreamVolume()分析

setStreamVolume()是SDK中提供给应用的API,它的作用是为特定的流类型设置范围内允许的任意音量。我们看一下它的实现:

看明白这个函数了吗?抛开被忽略掉的那个if块可以归纳为:这个函数的工作其实很简单,就执行了下面三方面的工作: 为调用setStreamVolumeInt()准备参数。 调用setStreamVolumeInt()。 广播音量发生变化的通知。

下面分析的主线将转向setStreamVolumeInt()的内容。

2.setStreamVolumeInt()分析

看一下setStreamVolumeInt()函数的代码,和前面一样,暂时忽略目前与分析目标无关的部分代码。

此函数有两个工作内容,一个是调用streamState.setIndex(),另一个则是根据setIndex()的返回值和force参数决定是否发送MSG_SET_DEVICE_VOLUME消息。这两项内容在3.2.1节中已经介绍过,在此不再赘述。

至此,setStreamVolume()的分析完成。注意分析完setStreamVolume()的工作流程后,读者是否觉得有些熟悉呢?如果我们用setStreamVolumeInt()的代码替换setStreamVolume()中对setStreamVolumeInt()的调用,再和adjustStreamVolume()函数进行以下比较,就会发现它们的内容出奇得相似。Android在其他地方也有这样的情况。从这一点上来说,已经发展到4.1版本的Android源代码仍然不够精致。读者可以思考一下,有没有办法把这两个函数融合为一个函数呢?

到此,对于音量设置相关的内容就告一段落。接下来我们将讨论和音量相关的另一个重要的内容—静音。3.2.3 静音控制

静音控制的情况与音量调节有很大的不同。因为每个应用都有可能进行静音操作,所以为了防止状态发生紊乱,就需要为静音操作进行计数,也就是说多次静音后需要多次取消静音。

不过,进行了静音计数后还会引入另外一个问题。如果一个应用在静音操作(计数加1)后因为某种原因不小心崩溃了,那么将不会有人再为它进行取消静音的操作,静音计数无法再回到0,也就是说这个“倒霉”的流将被永远静音下去。

那么怎么处理应用异常退出后的静音计数呢?AudioService的解决办法是记录下每个应用自己的静音计数,当应用崩溃时,在总的静音计数中减去崩溃应用自己的静音计数,也就是说,为这个应用完成它没能完成的取消静音这个操作。为此,VolumeStreamState定义了一个继承自DeathRecepient的内部类,名为VolumeDeathHandler,并且为每个进行静音操作的进程创建一个实例。VolumeDeathHandler的实例保存了对应进程的静音计数,并在进程死亡时进行计数清零的操作。从这个名字来看可能是Google希望这个类将来能够承担更多与音量相关的事情吧,不过眼下它只负责静音。我们将在后续的内容中对这个类进行深入讲解。

经过前面的介绍,我们不难得出AudioService、VolumeStreamState与VolumeDeathHandler的关系,如图3-4所示。图3-4 与静音相关的类

1.setStreamMute()分析

同音量设置一样,静音控制也是相对于某一个流类型而言的。正如本节开头所提到的,静音控制涉及引用计数和客户端进程的死亡监控。所以相对于音量控制来说,静音控制有一定的复杂度。还好,静音控制对外入口只有一个函数,就是AudioManager.setStreamMute()。其第二个参数state为true,表示静音,否则表示解除静音。

AudioManager一如既往地充当着AudioService代理的一个角色,不过这次有一个很小却很重要的动作:AudioManager为AudioService传入了一个名为mICallBack的变量。查看一下mICallBack的定义:

真是简单得不得了。全文搜索一下,我们发现mICallBack只用来作为AudioService的几个函数调用的参数。从AudioManager角度看它没有任何实际意义。其实,这在Android的进程间交互通信中是一种常见且非常重要的技术。mICallBack这个简单Binder对象可以充当Bp端在Bn端的一个唯一标识。而且AudioService拿到这个标识后,就可以通过DeathRecipient机制获取Bp端异常退出的回调。这是AudioService维持静音状态正常变迁的一个基石。注意服务端把客户端传入的这个Binder对象作为客户端的一个唯一标识的时候,往往会以这个标识为键创建一个Hashtable,用来保存每个客户端的相关信息。这在Android各个系统服务的实现中是一种很常见的用法。另外,本例传入的mICallBack是直接从Binder类实例化出来的,是一个很原始的IBinder对象。进一步讲,如果传递了一个通过AIDL定义的IBinder对象,那么这个对象就有了交互能力,服务端可以通过它向客户端进行回调。在后面探讨AudioFocus机制时会遇到这种情况。

2.VolumeDeathHandler分析

我们继续跟踪AudioService.setStreamMute()的实现,记得注意第三个参数cb,它代表特定客户端的标识。

接下来是VolumeStreamState的mute()函数。VolumeStreamState的确是音量相关操作的核心类型。

上述代码引入了静音控制的主角,VolumeDeathHandler,也许叫做MuteHandler更合适一些。它其实只有两个成员变量,分别是mICallBack和mMuteCount。其中mICallBack保存了客户端传进来的标识,mMuteCount则保存了当前客户端执行静音操作的引用计数。另外,它继承自IBinder.DeathRecipient,所以它拥有监听客户端生命状态的能力。而VolumeDeathHandler()的成员函数只有两个,分别是mute()和binderDied()。说到这里,再看看上面VolumeStreamState.mute()的实现,读者能想象到VolumeDeathHandler的具体实现是什么样子的吗?

继续上面的脚步,看一下它的mute()函数。它的参数state的取值指定了进行静音还是取消静音。所以这个函数也就被分成两部分,分别是处理静音与取消静音两个操作。其实,这完全可以放在两个函数中完成。先看看静音操作是怎么实现的吧。

看明白了吗?这个函数的条件嵌套比较多,仔细归纳一下,就会发现这段代码的思路是非常清晰的。静音操作根据条件满足与否,完成三个任务: 无论在什么条件下,只要执行这个函数,静音操作的引用计数都

会加1。 如果这是客户端第一次执行静音,则开始监控其生命状态,并且

把自己加入VolumeStreamState的mDeathHandlers列表中。这是

这段代码中很精练的一个操作,只有在客户端执行过静音操作后

才会对其生命状态感兴趣,才有保存其VolumeDeathHandler的

必要。 更进一步的是,如果这是这个流类型第一次被静音,则设置流音

量为0,这才是真正的静音动作。

不得不说,这段代码是非常精练的,不是说代码量少,而是它的行为非常干净,决不会做多余的操作,也不会保存多余的变量。

下面我们要看一下取消静音的操作。取消静音作为静音的逆操作,相信读者已经可以想象到它都做什么事情了吧?这里就不再对其进行说明了。

下面就剩下最后的binderDied()函数了。当客户端发生异常,没能取消其执行过的静音操作时,需要替它完成它应该做却没做的事情。

这个实现不难理解,读者可以自行分析一下为什么这么做可以消除意外退出的客户端遗留下来的影响。3.2.4 音量控制小结

音量控制是AudioService最重要的功能之一。经过上面的讨论,相信读者对AudioService的音量管理流程已经有了一定的理解。

总结一下我们在这一节里所学到的内容: AudioService音量管理的核心是VolumeStreamState。它保存了

一个流类型所有的音量信息。 VolumeStreamState保存了运行时的音量信息,而音量的生效则

是在底层AudioFlinger完成的。所以进行音量设置需要做两件事

情:更新VolumeStreamState存储的音量值,设置音量到Audio底

层系统。 VolumeDeathHandler是VolumeStreamState的一个内部类。它的

实例对应在一个流类型上执行静音操作的一个客户端,是实现静

音功能的核心对象。3.3 音频外设的管理

这一节将探讨AudioService的另一个重要功能,那就是音频外设的管理。看过卷I第7章的读者应该对音频外设这个概念并不陌生。在智能机全面普及的时代,对有线耳机、蓝牙耳机等音频外设的支持已经是手机的标准,有些机型甚至支持HDMI、USB声卡等输出接口。再加上手机本身自带的扬声器与听筒,这样一来,一台手机上同时能进行音频输出的设备往往会有三四种甚至更多。如何协调这些设备的工作,使其符合用户的使用习惯、满足用户的需求变得非常重要。

卷I的第7章详细介绍过AudioPolicy如何进行设备的路由切换,然而并没有讨论音频设备为什么出现在AudioPolicy的设备候选列表中,这一节将以有线耳机为例讨论这个问题。3.3.1 WiredAccessoryObserver设备状态的监控

1.WiredAccessoryObserver简介

这要从WiredAccessoryObserver开始讲起,它是内核通知有线耳机插入事件所到达的第一个环节。

WiredAccessoryObserbver继承自UEventObserver。UEventObserver是Android用来接收UEvent的一个工具类。UEventObserver类维护着一个读取UEvent的线程,注意这个线程是UEventObserver的一个静态成员,也就是说,一个进程只有一个。当调用UEventObserver的startObserving()函数开始监听时,会告诉这个线程UEventObserver关心什么样的UEvent,当匹配的事件到来时,监听线程会通过回调UEventObserver的onUEvent函数进行通知。读者可以看一下UEventObserver的源代码以了解其具体实现,这并不复杂。

WiredAccessoryObserver接收内核上报的和耳机/HDMI/USB相关的UEvent事件,并将其翻译成设备的状态变化。由于每种外设都有自己的UEvent与状态文件,因此WiredAccessoryObserver定义了一个内部类名为UEventInfo,并且为自己感兴趣的每一个音频外设创建一个实例,其内部保存了对应外设的名字、UEvent地址及状态文件的地址。每当有合适的UEvent到来时,WiredAccessoryObserver就会查找匹配的UEventInfo实例,并且更新可用设备的状态列表,同时通知AudioService。

关于可用外设的状态列表,虽然称为列表,事实上,它只是一个整型的变量,名为mHeadsetState。在可用外设的状态列表中用一个二进制标志位表示某个外设的状态可用与否,这与AudioPolicyManager的mAvailableOutputDevices的用法是一样的。下面是各种外设的标志位的定义:

举个例子,如果mHeadsetState等于0x00000002,也就是BIT_HEADSET_NO_MIC,表示目前手机上插入一个不带麦克风的耳机。而如果mHeadsetState等于0x00000011,也就是HEADSETS_WITH_MIC|BIT_HDMI_AUDIO,则表示目前手机上同时插入一个带有麦克风的耳机及HDMI输出线。

WiredAccessoryObserver工作原理就这么简单,我们接下来将以有线耳机为例子对其进行详细讨论。

2.启动与初始化

虽然WiredAccessoryObserver不是一个服务,但是它拥有系统服务的待遇—在system_server中同系统服务一起被加载,如下所示:

只有一个构造函数,其实,构造函数中并没有做太多的初始化工作,而是注册了一个BroadcastReceiver,监听ACTION_BOOT_COMPLETE。其真正的初始化工作是在这个BootCompletedReceiver中完成的。

这里的init()函数的作用是为了在开机后对外设的状态进行初始化。

到这里WiredAccessoryObserver已经完成初始化了,已经对第一条UEvent的到来准备就绪。

3.耳机插入或拔出时的处理

如果有外设被插入或拔出,WiredAccessoryObserver的onUEvent()函数会被回调。参数event中保存了其详细的信息。

看到这里,读者是否觉得updateState的实现有些笨拙了呢?如果以devName为键,将uEventInfo保存在Hashtable中,无论对代码的整洁还是执行的效率都是有帮助的。

注意uei.computeNewHeadsetState()这个函数,它的目的是通过UEvent上报的状态值计算出新的可用外设列表。注意computeNewHeadsetState()这个函数的扩展性并不是太好,只是目前够用而已,读者可以自行研究。

继续前面的脚步,现在到了update()函数。这个函数的目的是对前面传入的newState进行全面检查,防止出现不正确的状态。这个函数的运算稍多些,为了方便分析,仅留下和有线耳机(h2w)相关的代码。注意这个函数的意图比较很明显,只是其中一个判断条件让人一时摸不着头脑,(h2w_headset&(h2w_headset-1))!=0。按照注释中的说法,此函数不接受同时有两种耳机出现的情况,也就是h2w_headst==BIT_HEADSET|BIT_HEADSET_NO_MIC,直接做这个判断不就可以了吗?仔细琢磨就能发现写这个条件的人的聪明之处。直接判断仅限于只有两种可能的外设时才能起作用,超过两个就很难处理了。而谷歌的这个做法既快捷,又可以应对任意多种可能的外设。读者可以思考一下为什么。另外,这段代码在执行mHandler.sendMessage()的调用之前先申请了一个电源锁。这是一个很细节但很重要的做法。当发送消息给一个Handler时,必须考虑设备有可能在Handler得以处理消息之前进入深睡眠状态的极端情况(对延时消息来说,可能就是常见情况了)。在这种情况下,CPU将会进入休眠状态,从而使得消息无法得到及时处理,影响程序执行的正确性。

可用外设列表更新完毕后发送了一条消息给mHandler。当消息生效时,直接调用setDevicesState()函数,它会遍历所有SUPPORTED_HEADSET,然后对每个外设调用setDeviceState()。注意,这两个函数是devices与device的区别。setDeviceState()的目的就是要把指定外设的状态汇报给AudioService,我们看一下它的实现:

之后,程序的流程将会离开WiredHeadsetObserver,再次前往AudioService。

4.总结一下WiredAccessoryObserver

对WiredHeadsetObserver的分析就先告一段落,这里再简单回顾一下关于它的知识。 它是站在最前方的一个哨兵,时刻监听着和音频外设拔插相关的

UEvent事件。 它接收到UEvent事件后,会翻译事件的内容为外设可用状态的

变化。 它是为AudioService服务的,一旦有变化就立刻通知

AudioService。 它虽然不是一个服务,但是它却运行在system_server中。 它不是唯一的音频外设状态监听者,它只负责监控有线连接的音

频外设。其他的,如蓝牙耳机,在其他相关模块中维护。但是它

们的本质是类似的,最终都要通知给AudioServic。有兴趣的读

者可以自行研究。3.3.2 AudioService的外设状态管理

最终还是要回到AudioService中来,它才是音频相关操作的主基地。

1.处理来自WiredAccessoryObserver的通知

AudioService会如何处理外设的可用状态变化呢?仔细想想,在开发播放器的时候一定接触过ACTION_AUDIO_BECOMING_NOISY和ACTION_HEADSET_PLUG这两个广播吧。另外,更重要的是,这些变化需要让底层的AudioPolicy知道。所以,笔者认为AudioService外设状态管理分为三个内容: 管理发送ACTION_AUDIO_BECOMING_NOISY广播。 发送设备状态变化的广播,通知应用。 将其变化通知底层。

从WiredHeadsetObserver调用的setWiredDeviceConnectionState()函数开始:

此函数负责两项工作:调用checkSendBecomingNoisyIntent()函数及发送SET_WIRED_DEVICE_CONNECTION_STATE消息给mAudioHandler。

checkSendBecomingNoisyIntent()函数的目的是判断当前状态的变化是否有必要发送BECOMING_NOISY广播。这个广播用于警告所有媒体播放应用声音即将从手机外放中进行播放。在绝大部分情况下,收到这个广播的应用都应当立即暂停播放,以避免用户无意识地泄露自己的隐私或打扰到周围的其他人。另外,这个函数的返回值决定了SET_WIRED_DEVICE_CONNECTION_STATE消息是否需要延时处理。其代码如下:

代码不长,有价值的内容不少。BECOMING_NOISY广播发出的条件是最后一个安静外设被拔出,这个很好理解。而推迟MSG_SET_WIRED_DEVICE_CONNECTION_STATE消息的生效时间这种做法可能一时难以弄明白。不过暂时先不管它,等我们了解了外设连接状态变化的流程后再解释它的意义。

回到setWiredDeviceConnectionState(),调用checkSendBecomingNoisyIntent()函数后,它发送MSG_SET_WIRED_DEVICE_CONNECTION_STATE给mAudioHandler,此消息生效后, mAudioHandler调用onSetWiredDeviceConnectionState函数。

在这个函数中,我们需要重点关注的是对handleDeviceConnection()和sendDevice-ConnectionIntent两个函数的调用。它们分别用来通知AudioPolicy与上层应用。

另外,还可以看到,在handleDeviceConnection()函数上下有一对关于蓝牙耳机的操作。从其实现上可以看出,如果拔出普通耳机,系统将会强制使用蓝牙耳机进行输出。如果插入耳机则会取消这个设置。这种操作完全可以放在AudioPolicyManager中实现。

看一下通知AudioPolicy的handleDeviceConnection()函数的实现吧!

很简单吧?如果读者对卷I第7章的内容比较熟悉,那么一定知道AudioSystem. setDeviceConnectionState()这个函数意味着什么。它将更新底层的AudioPolicy中缓存的可用设备列表,同时,如果正在进行音频播放,那么这个函数还将触发音频设备的重新选择。注意这一节提到“可用设备列表”的次数很多,很多地方都使用了这个概念。归纳一下,在本节所讨论的内容里,有三个地方有可用设备列表:1)WiredAccessoryObserver: 目的是确认外设的状态变化是否合法,是否需要报告给AudioService。2)AudioService: 它以一个Hashtable的形式保存了一个可用设备列表,它为AudioService向应用及底层AudioPolicyManager发送通知提供依据。3)AudioPolicyManager: 它保存的可用设备列表在AudioPolicyManager需要重新选择音频输出设备时提供候选。

2.关于推迟处理外设状态

前面讨论checkSendBecomingNoisyIntent()函数的实现时提到了根据某些条件,有可能使MSG_SET_WIRED_DEVICE_CONNECTION_STAT延迟生效1秒。在这种情况下应用会在1秒之后才能收到设备状态变化的广播,同时,AudioPolicy也要在1秒之后才能更新可用设备列表并进行必要的设备切换。为什么要这么做呢?想想推迟的条件: 最后一个安静外设被移除,发送了BECOMING_NOISY广播。 队列中尚有两个消息在等候处理:

MSG_SET_WIRED_DEVICE_CONNECTION_STATE和

MSG_SET_A2DP_CONNECTION_STATE。

只要这两个条件有一个满足,就会发生1秒推迟。下面分别讨论。

关于第一个条件,当最后一个安静外设被移除后,手机上可用的音频输出设备就只剩下扬声器了(听筒不能算是常规的音频输出设备,它只有在通话过程中才会用到)。那么在MSG_SET_WIRED_DEVICE_CONNECTION_STAT生效后,AudioPolicyManager将会切换输出到扬声器,此时正在播放的音频就会被外放出来。

很多时候,这并不是用户所期望的,用户可能不希望他人知道自己在听什么,或者不希望在某些场合下扬声器发出的声音打扰到其他人。何况耳机被拔除有可能还是个意外。所以,正在进行音频播放的应用可能希望收到耳机等安静设备被拔出时的通知,并且在收到后暂停播放。

读者可能会有疑问,在sendDeviceConnectionIntent()中不是发送了状态通知的广播了吗?其实,这个状态通知广播用在其他情况下可以,但是用在上述情况中是有问题的。按照上面的讨论,执行sendDeviceConnectionIntent()之前,先执行了handleDeviceConnection(),它会更新底层的可用设备列表,并且触发设备切换。于是应用有可能在收到状态通知之前,输出设备已经被切换成扬声器了,直到应用收到通知后暂停回放,这段时间内就会发生扬声器的漏音。

所以,Android引入了一个新的广播来应对这个问题,那就是BECOMING_NOISY广播。这个广播只有在最后一个安静外设被移除后才会发出,于是应用可以精确地知道音频即将从扬声器进行播放,而且后续的设备切换等动作被推迟了1秒,应用就有充足的时间收到BECOMING_NOISY广播并暂停播放。在正常情况下,这种做法可以杜绝漏音的情况出现。这是第一个延时条件的意义。

至于第二个条件,队列中尚有以下两个消息等候处理:MSG_SET_WIRED_DEVICE_CONNECTION_STATE和MSG_SET_A2DP_CONNECTION_STATE,这其实是不得已的一种做法。考虑一下,为什么队列中尚有这两个消息在等候处理呢?一个是mAudioHandler所在的线程发生了阻塞,另一个就是这两个消息被延迟发送了。根据Handler现有的接口没有办法得知是哪一种情况,但是在正常情况下都是第二种,也是比较麻烦的一种情况。因为在这种情况下,如果正常发送MSG_SET_WIRED_DEVICE_CONNECTION_STATE消息,那么它的生效时间将会早于正在队列中排队的那两个消息。如此一来,就会发生外设可用状态紊乱的问题。所以,AudioService迫不得已在这种情况下推迟发送1秒。读者可以做个试验,快速地在手机上拔插耳机,将会看到通知栏内的耳机图标的变化总是会延迟1秒。注意我们在之前的分析中没有见过MSG_SET_A2DP_CONNECTION_STATE,它和讨论的MSG_SET_WIRED_DEVICE_CONNECTION_STATE意义是一样的,而且有着几乎相同的处理逻辑,不过它是与蓝牙耳机相关的。3.3.3 音频外设管理小结

这一节以有线音频外设为例,探讨了从WiredAccessoryObserver收到UEvent开始到AudioService通知底层应用为止的AudioService对音频外设的管理机制。

总结一下音频外设拔插的处理过程: 由负责相关外设的模块监听从硬件上报的状态通知。将状态变化

提交给AudioService进行处理。 AudioService得到相关模块发来的通知,根据需要发送

BECOMING_NOISY消息给应用,并更新自己的可用设备列表。 AudioService将外设可用状态的变化通知AudioPolicy。

AudioPolicy更新自己的可用设备列表,并重新选取音频输出设

备。 AudioService将外设可用状态以广播的形式发送给应用等其他对

此感兴趣的应用程序或系统模块。

蓝牙模块负责蓝牙耳机的连接/断开状态的监控并通知AudioService。AudioService收到此通知之后的代码路径虽然与本节所讨论的内容不完全一样,但其处理原则与有线耳机是一致的,读者可以自行分析学习。3.4 AudioFocus机制的实现

AudioFocus是自Android 2.3建立起来的一个新的机制。这套新机制的目的在于统一协调多个回放实例之间的交互。

我们知道,手机的多媒体功能越来越强大,听音乐、看视频、听收音机已经成为这台小小的设备的重要功能。加上手机本身的闹铃、信息通知以及电话铃声等,一台手机中有很多情况需要播放音频。我们称每一次音频播放为一次回放实例。这就需要我们能够对这些回放实例的并发情况做好协调,否则就会出现多个音频不合理地同时播放的恼人结果。

在2.3以前,Android并没有一套统一的管理机制。每个音频回放实例只能通过发送广播的方式告知其他人自己的播放状态。这不仅造成了广播满天飞的情况,而且可扩展性与一致性非常差,基本上只能在同一厂商的应用之间使用。好在,Android 2.3对AudioFocus的引入大大地改善了这个状况。

AudioFocus的含义可以和Windows的窗口焦点机制做类比,只不过我们的焦点对象是音频的回放实例。在同一时间,只能有一个音频回放实例拥有焦点。每个回放实例开始播放前,必须向AudioService申请获取AudioFocus,只有申请成功才允许开始回放。在回放实例播放结束后,要求释放AudioFocus。在回放实例播放的过程中,AudioFocus有可能被其他回放实例抢走,这时,被抢走AudioFocus的回放实例需要根据情况采取暂停、静音或降低音量的操作,以突出拥有AudioFocus的回放实例的播放。当AudioFocus被还回来时,回放实例可以恢复被抢走之前的状态,继续播放。

总体上来说,AudioFocus是一个没有优先级概念的抢占式的机制。在一般情况下后一个申请者都能从前一个申请者的手中获取AudioFocus。不过只有一个例外,就是通话。通话作为手机的首要功能,同时也是一种音频的播放过程,所以从来电铃声开始到通话结束这个过程, Telephony相关的模块也会申请AudioFocus,但是它的优先级是最高的。Telephony可以从所有人手中抢走AudioFocus,但是任何人无法从它手中将其夺回。这在后面的代码分析中可以看到。

值得一提的是,AudioFocus机制完全是一个建议性而不是强制性的机制。也就是说,上述的行为是建议回放实例遵守,而不是强制的。所以,市面上仍有一些带有音频播放功能的应用没有采用这套机制。3.4.1 AudioFocus最简单的例子

AudioFocus相关的API一共有三个,分别是requestAudioFocus()、abandonAudioFocus()以及AudioFocusListener回调接口,它们分别负责AudioFocus的申请、释放以及变化通知。

为了让大家了解AudioFocus的实现原理,我们先来看一个AudioFocus使用方法。下面是一个播放器的部分代码:

在开始播放前,应用首先要申请AudioFocus。申请AudioFocus时传入了3个参数。 mAudioFocusListener:参数名为l。AudioFocus变化通知回调。

当AudioFocus被其他AudioFocus使用者抢走或归还时将通过这个

回调对象进行通知。 AudioManager.STREAM_MUSIC: 参数名为streamType。这个参

数表明了申请者即将使用哪种流类型进行播放。目前这个参数仅

提供一项信息而已,对AudioFocus的机制没有任何影响,不过

Android在后续的升级中可能会使用此参数。 AudioManager.AUDIOFOCUS_GAIN: 参数名为durationHint。这

个参数指明了申请者将拥有这个AudioFocus多长时间。例子中传

入的AUDIOFOCUS_GAIN表明了申请者将长期占用这个

AudioFocus。另外还有两个可能的取值,它们是

AUDIOFOCUS_GAIN_TRANSIENT和

AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK。这两个取值的

含义都表明申请者将暂时占用AudioFocus,不同的是,后者还指

示了即将被申请者抢走AudioFocus的使用者不需要暂停,只要降

低一下音量就可以了(这就是“DUCK”的意思)。

在停止播放时,需要调用abandonAudioFocus释放AudioFocus,会将其归还给之前被抢走AudioFocus的那个使用者。

接下来,我们看一下mAudioFocusListener是如何实现的。

从这里能够看出,AudioFocus机制的逻辑是完整而清晰的。理解上述例子以后,相信AudioFocus的工作原理已经浮现在脑海里了吧?如果有兴趣,可以不用着急阅读下面的分析,先思考一下AudioFocus可能的工作原理,然后再与Android的实现进行比较。注意从调用requestAudioFocus()进行申请到abandonAudioFocus()释放的这段时间内,只能说这个使用者参与了AudioFocus机制,不能保证一直拥有AudioFocus。3.4.2 AudioFocus实现原理简介

看了前面的示例代码,可以推断出,AudioFocus的实现基础应该是一个栈。栈顶的使用者拥有AudioFocus。

在申请AudioFocus成功时,申请者被放置在栈顶,同时,通知之前在栈顶的使用者,告诉它新的申请者抢走了AudioFocus。

当释放AudioFocus时,使用者将从栈中被移除。如果这个使用者位于栈顶,则表明释放前它拥有AudioFocus,因此AudioFocus将被返还给新的栈顶。

工作原理参考图3-5。图3-5AudioFocus的工作原理

AudioFocus的工作原理就是如此。下面将分别对申请和释放这两个过程进行详细分析。3.4.3 申请AudioFocus

先看一下AudioFocus的申请过程。

这段代码有两个关键的地方,分别是调用registerAudioFocusListener()和调用Audio-Service的requestAudioFocus()函数。这个函数的参数很多,随着分析的深入,这些参数的意思会逐渐明朗。

下面先看一下registerAudioFocusListener做了什么。

原来,应用程序提供的AudioFocusChangeListener是被注册进AudioManager的一个Hashtable中而不是AudioService中。注意,这个Hashtable的KEY是getIdForAudioFocus-Listener()分配的一个字符串型的Id。这样看来,AudioManager这一侧一定有一个代理负责接受AudioService的回调并从这个Hashtable中通过Id将回调转发给相应的Listener。

究竟是谁在做这个代理呢?就是稍候将作为参数传递给AudioService的mAudio-FocusDispatcher。

可以看出,mAudioFocusDispatcher的实现非常轻量级,直接把focusChange和listener的id发送给了一个Handler去处理。注意这个看似繁冗的做法其实是很有必要的。要知道,目前这个回调尚在Binder的调用线程中,如果在这里因为用户传入的Listener的代码有问题而报出异常或阻塞甚至恶意拖延,则会导致Binder的另一端因异常而崩溃或阻塞。到这里为止,AudioService已经尽到了通知义务,应该通过Handler将后续的操作发往另一个线程,使AudioService尽可能远离回调实现的影响。

看一下这个Handler的消息处理函数,msg.what存储了focusChange参数,msg.obj1存储了Id。因此handleMessage函数一定像下面这个样子:

现在我们了解了AudioService的回调是如何传递给回放实例的。概括来说, mAudioFocusDispather作为AudioService与AudioManager的沟通桥梁,将回调操作以消息的方式发送给mFocusEventHandlerDelegate的Handler,在Handler的消息处理函数中通知回放实例。注意读者也许会有疑问,为什么不让AudioFocusChangeListener直接继承自AIDL描述的接口,非要由mAudioFocusDispatcher去做转发,这不是很麻烦呢?这是为了节约Binder的资源。虽说mAudioFocusDispatcher不是一个服务,但是其对Binder资源的占用却与服务一样,所以大量使用Binder回调是有待商榷的。这种把多个回调的信息保存在Bp端,使用一个拥有Binder通信能力的回调对象做它们的代理是一种很值得推荐的做法。

回到AudioManager.requestAudioFocus()的实现中。我们调用AudioService.request-AudioFocus()时传入的参数很多。它们都是什么意义呢?为了方便后面探讨,我们先把AudioService.requestAudioFocus的参数意义搞清楚。 mainStreamType: 这个参数目前没有被使用,这是为了便于以后

功能扩展所预留的一个参数。 focusChangeHint:这个参数指明了申请者持有AudioFocus的方

式。 cb:IBinder类型的一个参数,在探讨静音控制时曾经见过这个参

数,就是AudioManager的mICallBack。应该能够联想到,

AudioService又要做linkTo-Death了。 fd:IAudioFocusDispatcher对象,我们刚刚分析过,它是

AudioService回调回放实例的中介。 clientId:参考AudioManager.requestAudioFocus()的实现,它是

通过getget-IdForAudioFocusListener()函数获取的一个字符串,

用于唯一标识一个Audio-FocusChangeListener。 callingPackageName: 回放实例所在的包名。注意这个Id真的是唯一的吗?getIdForAudioFocusListener()返回的其实就是一个toString()。这样做是无法严格区分两个不同的AudioFocusListener实例的。

接下来看看AudioService.requestAudioFocus()的工作原理。

代码比较长,但是思路还是比较清晰的。总结一下申请AudioFocus的工作内容: 通过canReassignAudioFocus()判断当前是否在通话中,如果在

通话过程中,则直接拒绝申请。具体如何让通话拥有最高优先级

的问题可参考canReassignAudioFocus()的实现。 对申请者进行linkToDeath。使得在申请者意外退出后可以代其

完成abandon-AudioFocus操作。 对于已经持有AudioFocus的情况,如果没有改变持有方式,则不

作任何处理,直接返回申请成功。否则将其从栈顶删除,暂时将

栈顶让给下一个回放实例。 通过回调告知此时处于栈顶的回放实例,它的AudioFocus将被夺

走。 将申请者的信息加入栈顶,成为新的拥有AudioFocus的回放实

例。

申请AudioFocus的方式已经了解,那么释放AudioFocus是什么样的一个流程呢?读者可以自己先思考一下。3.4.4 释放AudioFocus

先看AudioManager的anbandonAudioFocus()函数,从这个函数中可以看出,向Audio-Service申请释放AudioFocus需要提供两个“证件”:mAudioFocusDispatcher和AudioFocus-ChangeListener的ID。

AudioService的abandonAudioFocus并没有更多的内容,只是调用了removeFocus-StackEntry()函数而已。参考requestAudioFocus的实现过程,可以推断这个函数的工作有: 从mFocusStack中删除拥有指定clientId的回放实例的信息。 执行unlinkToDeath,取消监听其死亡通知。 如果被删除的回放实例位于栈顶的位置,说明AudioFocus还给了

另外一个回放实例,这时就要通过它的mFocusDispatcher回调,

通知它重新获得了AudioFocus。

removeFocusStackEntry()的工作就是如此,只是实现得不够简练。注意看完AudioFocus的申请与释放的实现代码,读者能否感受到它们的实现在细节上确实有些臃肿和重复。对比曾经分析过的静音控制相关的代码,实在差距不小。阅读Android源代码的时候,我们不仅仅是在学习某些功能的原理,同时也是博取其代码组织与书写的精妙之处,发现其不足的地方并引以为戒。关于AudioFocus,读者是否有自己的想法改造其实现,让其更加精炼吗?3.4.5 AudioFocus小结

这一节学习了AudioFocus机制的工作原理。AudioFocus机制有三部分内容:申请、释放与回调通知,这些内容都是围绕一个名为mFocusStack的栈完成的。

在对代码的分析过程中,可以看到AudioFocus基本上是自成一个小的系统,没有和外部服务,尤其是Audio底层打过交道,而且AudioFocus的回调通知只是告诉回放实例AudioFocus发生了变化,无法保证回放实例在回调中做什么。这说明了AudioFocus作为一个协调工具,是没有任何强制力的。希望在以后版本的Android中AudioFocus可以适当地增加一些约束能力使得这套机制可以发挥更大的作用。

即便如此,AudioFocus作为唯一的通用的音频交互策略,建议每一个涉及音频播放的应用都能参与这套机制,并且认真遵守其规则,这样才能保证Android音频“社会”的和谐。3.5 AudioService的其他功能

这一章已经介绍了音量控制、外设管理及AudioFocus几个常用重要功能的实现。然而, AudioService仍然有很多其他相互独立的功能。限于篇幅,这里没有办法一一详细说明。在这里简单介绍一下,以便读者自行研究。(1)RemoteConrolClient/Display机制

RemoteControlClient/Display是从Android 4.0引入的一套新机制。它定义了一个远程控制端、一个远程显示端。这使得媒体播放过程中的元数据(例如标题、艺术家等)与其他信息可以跨应用显示。远程控制端由进行播放的应用管理,而远程显示端被一个显示界面管理,比如说一个AppWidget。由AudioService作为中介为它们进行配对与数据传递。(2)MediaButton的管理

所谓的MediaButton是指线控耳机上的一个按键,虽然耳机线上只有这一个按键,但是它的功能却异常得多,例如接听/挂断电话,启动音乐播放器,暂停/继续/下一首,等等。加上其使用方便,很多应用,尤其是播放器,争相操作(Handle)这个按键的事件。AudioService就是为了能够协调争抢这个按键的应用才插手管理这个按键的派发。(3)指定声音的输出设备

这个功能在AudioManger中表现为一系列名为setXXXOn的函数,其中的XXX表示了一个音频输出设备的名字。它们其实都使用了AudioService的setForceUse()函数。准确地说, AudioService并没有为这个功能做过实际工作,只是作为应用到AudioPolicy的一个中介。(4)音效管理

AudioService在启动时,会使用SoundPool工具预加载一系列的音效文件,用于系统中的一些短小而频繁的音频播放,比如按键音。

SoundPool的工作原理是什么呢?在初始化时,AudioService要求SoundPool加载所需的音频文件。SoundPool会把这些音频文件解码为PCM音频流并缓存。同时为每段音频流分配一个ID,每当AudioService需要播放一段音效时,把对应的ID传递给SoundPool,SoundPool就会找到这块缓存的音频流,通过AudioTrack直接写入AudioFlinger中,实现音效播放。相对于MediaPlayer,由于每次播放时省却了prepare与编解码的过程,因此效率比其高很多,很适合用在游戏等对声音的及时性要求很高的场合。问题是,这个工具太消耗内存了。(5)情景模式

情景模式和音量的关联是比较紧密的,或者说,情景模式是在音量控制的基础上实现的一个功能。(6)音频状态管理

在AudioService中就是两个函数:getMode()与setMode()。音频状态表示了手机的4种状态,待机状态、音频通话状态、视频/VoIP通话状态与响铃状态。这4种状态对底层的音频输出设备的选择影响很大,同时也影响了AudioService的一些行为,例如MediaButton的管理。3.6 本章小结

这一章介绍了AudioService的几个重要的功能,相信大家通过这章对Audio系统在Java Famework层面所做的事情有了一个比较深入的了解。由于AudioService的功能太过繁杂,本章只能将几个有代表意义并且实际接触比较多的内容进行讲解与探讨。若想更加了解AudioService及其周边模块的工作原理仍需要读者不懈努力。

另外,AudioService的一些功能都涉及AudioPolicy的相关内容,所以在学习本章时要多参考AudioPolicy的相关知识。第4章 深入理解WindowManagerService

本章主要内容: 介绍最原始、最简单的窗口创建方法 研究WMS的窗口管理结构 探讨WMS布局系统的工作原理 研究WMS动画系统的工作原理

本章涉及的源代码文件名及位置: SystemServer.java

frameworks/base/services/java/com/android/server/SystemServer.java WindowManagerService.java

frameworks/base/services/java/com/android/server/wm/WindowManagerService.java ActivityStack.java

frameworks/base/services/java/com/android/server/am/ActivityStack.java WindowState.java

frameworks/base/services/java/com/android/server/wm/WindowState.java PhoneWindowManager.java

frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java AccelerateDecelerateInterpolator.java

frameworks/base/core/java/android/view/animation/AccelerateDecelerateInterpolator.java Animation.java

frameworks/base/core/java/android/view/animation/Animation.java AlphaAnimation.java

frameworks/base/core/java/android/view/animation/AlphaAnimation.java WindowAnimator.java

frameworks/base/services/java/com/android/server/wm/WindowAnimator.java WindowStateAnimator.java

frameworks/base/services/java/com/android/server/wm/WindowStateAnimator.java4.1 初识WindowManagerService

WindowManagerService(以下简称WMS)是继ActivityManagerService与Package-ManagerService之后又一个复杂却十分重要的系统服务。

在介绍WMS之前,首先要了解窗口(Window)是什么。

Android系统中的窗口是屏幕上的一块用于绘制各种UI元素并可以响应用户输入的一个矩形区域。从原理上来讲,窗口的概念是独自占有一个Surface实例的显示区域。例如Dialog、Activity的界面、壁纸、状态栏以及Toast等都是窗口。

卷I第8章曾详细介绍了一个Activity通过Surface来显示自己的过程: Surface是一块画布,应用可以随心所欲地通过Canvas或者

OpenGL在其上作画。 然后通过SurfaceFlinger将多块Surface的内容按照特定的顺序(Z-order)进行混合并输出到FrameBuffer,从而将Android“漂

亮的脸蛋”显示给用户。

既然每个窗口都有一块Surface供自己涂鸦,必然需要一个角色对所有窗口的Surface进行协调管理。于是WMS应运而生。WMS为所有窗口分配Surface,掌管Surface的显示顺序(Z-order)以及位置尺寸,控制窗口动画,并且还是输入系统的一个重要中转站。说明一个窗口拥有显示和响应用户输入这两层含义,本章将侧重于分析窗口的显示,而响应用户输入的过程则在第5章中详细介绍。

本章将深入分析WMS的两个基础子系统的工作原理: 布局系统(Layout System):计算与管理窗口的位置、层次。 动画系统(Animation System):根据布局系统计算的窗口位置

与层次渲染窗口动画。

为了让读者对WMS的功能以及工作方式有一个初步认识,并见识一下WMS的强大,本节将从一个简单而神奇的例子开始WMS的学习之旅。4.1.1 一个从命令行启动的动画窗口

1.SampleWindow的实现

在这一节里将编写一个最简单的Java程序SampleWindow,仅使用WMS的接口创建并渲染一个动画窗口。此程序将抛开Activity、Wallpaper等UI架构的复杂性,直接了当地揭示WMS的客户端如何申请、渲染并注销自己的窗口。同时这也初步地反映了WMS的工作方式。

这个例子很简单,只有三个文件: SampleWindow.java主程序源代码。 Android.mk编译脚本。 sw.sh启动器。

分别看一下这三个文件的实现:关于Choreographer,请参考卷I和卷II的作者邓凡平的博客文章《Android Project Butter分析》(http://blog. csdn.net/innost/article/details/8272867)。

2.初识窗口的创建、绘制与销毁

SampleWindow的这段代码虽然简单,但是却很好地提炼了一个窗口的创建、绘制以及销毁的过程。注意,本例没有使用任何WMS以外的系统服务,也没有使用Android系统4大组件的框架,也就是说,如果你愿意,可以利用WMS实现自己的UI与应用程序框架,这样就可以衍生出一个新的平台。

总结在客户端创建一个窗口的步骤: 获取IWindowSession和WMS实例。客户端可以通过

IWindowSession向WMS发送请求。 创建并初始化WindowManager.LayoutParams。注意这里是

WindowManager下的LayoutParams,它继承自

ViewGroup.LayoutParams类,并扩展了一些窗口相关的属性。

其中最重要的是type属性。这个属性描述了窗口的类型,而窗口

类型正是WMS对多个窗口进行ZOrder排序的依据。 向WMS添加一个窗口令牌(WindowToken)。本章后续将分析

窗口令牌的概念,目前读者只要知道,窗口令牌描述了一个显示

行为,并且WMS要求每一个窗口必须隶属于某一个显示令牌。 向WMS添加一个窗口。必须在LayoutParams中指明此窗口所隶

属于的窗口令牌,否则在某些情况下添加操作会失败。在

SampleWindow中,不设置令牌也可成功完成添加操作,因为窗

口的类型被设为TYPE_SYSTEM_ALERT,它是系统窗口的一

种。而对于系统窗口,WMS会自动为其创建显示令牌,故无须

客户端操心。此话题将会在后文进行具体讨论。 向WMS申请对窗口进行重新布局(relayout)。所谓的重新布局,

就是根据窗口新的属性去调整其Surface相关的属性,或者重新

创建一个Surface(例如窗口尺寸变化导致之前的Surface不满足

要求)。向WMS添加一个窗口之后,其仅仅是将它在WMS中进

行注册而已。只有经过重新布局之后,窗口才拥有WMS为其分

配的画布。有了画布,窗口之后就可以随时进行绘制工作了。

而窗口的绘制过程如下: 通过Surface.lock()函数获取可以在其上作画的Canvas实例。 使用Canvas实例进行作画。 通过Surface.unlockCanvasAndPost()函数提交绘制结果。提示关于Surface的原理与使用方法,请参考卷I第8章。

这是对Surface作画的标准方法。在客户端也可以通过OpenGL进行作画,不过这超出了本书的讨论范围。另外,在SampleWindow例子中使用了Choreographer类进行动画帧安排。Choreographer意为编舞指导,是Jelly Bean新增的一个工具类。其用法与Handler的post()函数非常之像,都会在后续的某个时机回调传入的Runnable对象。不同的它他们的回调时机有所差异。因为Handler回调时机取决于消息队列的处理情况,而Choreographer的回调时机则为下一次VSYNC(垂直刷新同步)。在WMS内部一样使用了Choreographer类进行窗口的平移、缩放、旋转等动画的渲染。

销毁窗口的操作则简单了很多,只要通过IWindowSession.remove()方法窗口从WMS中删除即可。如果客户端己经完成了自己的工作并且不会再显示新的窗口,则需要从WMS将之前添加的显示令牌一并删除。

3.窗口的概念

在SampleWindow例子中,有一个名为mWindow(类型为IWindow)的变量。读者可能会理所当然地认为它就是窗口了。其实这种认识并不完全正确。IWindow继承自Binder,并且其Bn端位于应用程序一侧(在例子中IWindow的实现类MyWindow就继承自IWindow. Stub),于是其在WMS一侧只能作为一个回调,以及起到窗口Id的作用。

那么,窗口的本质是什么呢?

是进行绘制所使用的画布:Surface。

当一块Surface显示在屏幕上时,就是用户所看到的窗口了。客户端向WMS添加一个窗口的过程,其实就是WMS为其分配一块Surface的过程,一块块Surface在WMS的管理之下有序地排布在屏幕上,Android才得以呈现出多姿多彩的界面。所以从这个意义上讲, WindowManagerService被称为SurfaceManagerService也说得通。

于是,根据对Surface的操作类型可以将Android的显示系统分为三个层次,如图4-2所示。图4-2Android显示系统的三个层次

在图4-2中: 第一个层次是UI框架层,其工作为在Surface上绘制UI元素以及

响应输入事件。 第二个层次为WMS,其主要工作是管理Surface的分配、层级顺

序等。 第三个层次为SurfaceFlinger,负责将多个Surface混合并输出。

经过这个例子的介绍,相信大家对WMS的功能有了一个初步了解。接下来,我们要进入WMS的内部,通过其启动过程一窥它的构成。4.1.2 WMS的构成

俗话说,一个好汉三个帮!WMS的强大是由很多重要的成员互相协调工作而实现的。了解WMS的构成将会为我们深入探索WMS打下良好的基础,进而分析它的启动过程,这是再合适不过了。

1.WMS的诞生

和其他的系统服务一样,WMS的启动位于SystemServer.java中ServerThread类的run()函数内。

由此可以看出,WMS的创建分为三个阶段: 创建WMS的实例。 初始化显示信息。 处理systemReady通知。

接下来,将通过以上三个阶段分析WMS从无到有的过程。

看一下WMS的main()函数的实现:注意Handler类在Android 4.2中新增了一个API:runWithScissors()。这个函数将会在Handler所在的线程中执行传入的Runnable对象,同时阻塞调用线程的执行,直到Runnable对象的run()函数执行完毕。

WindowManagerService.main()函数在ServerThread专为WMS创建的线程“Window-Manager”上创建了一个WindowManagerService的新实例。WMS中所有需要的Looper对象,例如Handler、Choreographer等,将会运行在“WindowManager”线程中。

接下来看一下其构造函数,看一下WMS定义了哪些重要的组件。关于Wi-Fi Display的详细信息,读者可参考http://blog.csdn.net/innost/article/details/8474683的介绍。

第二步,displayReady()函数的调用主要是初始化显示尺寸的信息。其内容比较琐碎,这里就先不介绍了。不过值得注意的一点是,在displayReady()完成后,WMS会要求ActivityManagerService进行第3一次Configuration更新。

第三步,在systemReady()函数中,WMS本身将不会再进行任何操作,直接调用mPolicy的systemReady()函数。

2.WMS的重要成员

总结一下在WMS的启动过程中所创建的重要成员,参考图4-3。

以下是对图4-3中重要成员的简单介绍:图4-3WMS的重要成员 mInputManager,InputManagerService(输入系统服务)的实例。

用于管理每个窗口的输入事件通道(InputChannel)以及向通道

上派发事件。关于输入系统的详细内容将在本书第5章详细探讨。 mChoreographer,Choreographer的实例,在SampleWindow的

例子中已经见过了。Choreographer的意思是编舞指导。它拥有

从显示子系统获取VSYNC同步事件的能力,从而可以在合适的

时机通知渲染动作,避免在渲染的过程中因为发生屏幕重绘而导

致的画面撕裂。从这个意义上讲,Choreographer的确是指导

Android翩翩起舞的大师。WMS使用Choreographer负责驱动所有

的窗口动画、屏幕旋转动画、墙纸动画的渲染。 mAnimator,WindowAnimator的实例。它是所有窗口动画的总管(窗口动画是一个WindowStateAnimator对象)。在

Choreographer的驱动下,逐个渲染所有的动画。 mPolicy,WindowPolicyManager的一个实现。目前它只有

PhoneWindowManager一个实现类。mPolicy定义了很多窗口相

关的策略,可以说是WMS的首席顾问!每当WMS要做什么事情

的时候,都需要向这个顾问请教应当如何做。例如,告诉WMS

某一个类型的Window的ZOrder的值是多少,帮助WMS矫正不合

理的窗口属性,会为WMS监听屏幕旋转的状态,还会预处理一

些系统按键事件(例如HOME、BACK键等的默认行为就是在这

里实现的),等等。所以,mPolicy可谓是WMS中最重要的一个

成员了。 mDisplayContents,一个DisplayContent类型的列表。Android 4.2

支持基于Wi-Fi Display的多屏幕输出,而一个DisplayContent描述

了一块可以绘制窗口的屏幕。每个DisplayContent都用一个整型

变量作为其ID,其中手机默认屏幕的ID由Display.

DEFAULT_DISPLAY常量指定。DisplayContent的管理是由

DisplayManagerService完成的,在本章不会去探讨

DisplayContent的实现细节,而是关注DisplayContent对窗口管理

与布局的影响。

下面的几个成员的初始化并没有出现在构造函数中,不过它们的重要性一点也不亚于上面几个。 mTokenMap,一个HashMap,保存了所有的显示令牌(类型为

WindowToken),用于窗口管理。在SampleWindow例子中曾经

提到过,一个窗口必须隶属于某一个显示令牌。在那个例子中所

添加的令牌就被放进了这个HashMap中。从这个成员中还衍生

出几个辅助的显示令牌的子集,例如mAppTokens保存了所有属

于Activity的显示令牌(WindowToken的子类AppWindowToken),

mExitingTokens则保存了正在退出过程中的显示令牌等。其中

mAppTokens列表是有序的,它与AMS中的mHistory列表的顺序

保持一致,反映了系统中Activity的顺序。 mWindowMap,也是一个HashMap,保存了所有窗口的状态信

息(类型为WindowState),用于窗口管理。在SampleWindow例

子中,使用IWindowSession. add()所添加的窗口的状态将会被保

存在mWindowMap中。与mTokenMap一样, mWindowMap一样

有衍生出的子集。例如mPendingRemove保存了那些退出动画播

放完成并即将被移除的窗口,mLosingFocus则保存了那些失去

了输入焦点的窗口。在DisplayContent中,也有一个windows列

表,这个列表存储了显示在此Display-Content中的窗口,并且它

是有序的。窗口在这个列表中的位置决定了其最终显示时的Z序。 mSessions,一个List,元素类型为Session。Session其实是

SampleWindow例子中的IWindowSession的Bn端。也就是说,

mSessions这个列表保存了当前所有想向WMS寻求窗口管理服务

的客户端。注意Session是进程唯一的。 mRotation,只是一个int型变量。它保存了当前手机的旋转状

态。

WMS定义的成员一定不止这些,但是它们是WMS每一种功能最核心的变量。读者在这里可以先对它们有一个感性认识。在本章后续的内容里将会详细分析它们在WMS的各种工作中所发挥的核心作用。4.1.3 初识WMS的小结

这一节通过SampleWindow的例子向读者介绍了WMS的客户端如何使用窗口,然后通过WMS的诞生过程简单剖析了WMS的重要组成成员,以期通过本节的学习能够为后续的学习打下基础。

从下一节开始,我们将会深入探讨WMS的工作原理。4.2 WMS的窗口管理结构

经过上一节的介绍,读者应该对WMS的窗口管理有了一个感性认识。从这一节开将深入WMS的内部去剖析其工作流程。

根据前述内容可知,SampleWindow添加窗口的函数是IWindowSession.add()。IWindowSession是WMS与客户端交互的一个代理,add则直接调用了WMS的addWindow()函数。我们将从这个函数开始WMS之旅。本小节只讨论它的前半部分。注意由于篇幅所限,本章不准备讨论removeWindow的实现。

addWindow()函数的前段代码展示了三个重要的概念,分别是WindowToken、WindowState以及DisplayContent。并且在函数开始处对窗口类型的检查判断也初步揭示了它们之间的关系:除子窗口外,添加任何一个窗口都必须指明其所属的WindowToken;窗口在WMS中通过一个WindowState实例进行管理和保管。同时必须在窗口中指明其所属的DisplayContent,以便确定窗口将被显示到哪一个屏幕上。4.2.1 理解WindowToken

1.WindowToken的意义

为了搞清楚WindowToken的作用是什么,看一下其位于WindowToken.java中的定义。虽然它没有定义任何函数,但其成员变量的意义却很重要。 WindowToken将属于同一个应用组件的窗口组织在一起。所谓的

应用组件可以是Activity、InputMethod、Wallpaper以及Dream。

在WMS对窗口的管理过程中,用WindowToken指代一个应用组

件。例如在进行窗口ZOrder排序时,属于同一个WindowToken的

窗口会被安排在一起,而且在其中定义的一些属性将会影响所有

属于此WindowToken的窗口。这些都表明了属于同一个

WindowToken的窗口之间的紧密联系。 WindowToken具有令牌的作用,是对应用组件的行为进行规范管

理的一个手段。WindowToken由应用组件或其管理者负责向

WMS声明并持有。应用组件在需要新的窗口时,必须提供

WindowToken以表明自己的身份,并且窗口的类型必须与所持有

的WindowToken类型一致。从前面的代码可以看到,在创建系统

类型的窗口时不需要提供一个有效的Token,WMS会隐式地为其

声明一个WindowToken,看起来谁都可以添加一个系统级的窗

口。难道Android为了内部使用方便而置安全于不顾吗?非也,

addWindow()函数一开始的mPolicy. checkAddPermission()的目的

就是如此。它要求客户端必须拥有SYSTEM_ALERT_WINDOW

或INTERNAL_SYSTEM_WINDOW权限才能创建系统类型的窗

口。

2.向WMS声明WindowToken

既然应用组件在创建一个窗口时必须指定一个有效的WindowToken才行,那么WindowToken究竟该如何声明呢?

在SampleWindow应用中,使用wms.addWindowToken()函数声明mToken作为它的令牌,所以在添加窗口时,通过设置lp.token为mToken向WMS出示,从而获得WMS添加窗口的许可。这说明,只要是一个Binder对象(随便一个),都可以作为Token向WMS进行声明。对WMS的客户端来说,Token仅仅是一个Binder对象而已。

为了验证这一点,来看一下addWindowToken的代码,如下所示:

使用addWindowToken()函数声明Token,将会在WMS中创建一个WindowToken实例,并添加到mTokenMap中,键值为客户端用于声明Token的Binder实例。与addWindow()函数中隐式地创建WindowToken不同,这里的WindowToken被声明为显式的。隐式与显式的区别在于,当隐式创建的WindowToken的最后一个窗口被移除后,此WindowToken会被一并从mTokenMap中移除。显式创建的WindowToken只能通过removeWindowToken()显式地移除。

addWindowToken()这个函数告诉我们,WindowToken其实有两层含义: 对显示组件(客户端)而言的Token,是任意一个Binder的实例,

对显示组件(客户端)来说仅仅是一个创建窗口的令牌,没有其

他的含义。 对WMS而言的WindowToken,这是一个WindowToken类的实例,

保存了对应于客户端一侧的Token(Binder实例),并以这个

Token为键,存储于mTokenMap中。客户端一侧的Token是否已

被声明,取决于其对应的WindowToken是否位于mTokenMap

中。注意在一般情况下,称显示组件(客户端)一侧Binder的实例为Token,而称WMS一侧的WindowToken对象为WindowToken。但是为了叙述方便,在没有歧义的前提下不会过分仔细地区分这两个概念。

接下来,看一下各种显示组件是如何声明WindowToken的。(1)Wallpaper和InputMethod的Token

Wallpaper的Token声明在WallpaperManagerService中。参考以下代码:

WallpaperManagerService是Wallpaper管理器,它负责维护系统已安装的所有Wall-paper并在它们之间进行切换,而这个函数的目的是准备显示一个Wallpaper。newConn.mToken与SampleWindow例子一样,是一个简单的Binder对象。这个Token将在即将显示的Wallpaper被连接时传递给它,之后Wallpaper即可通过这个Token向WMS申请创建绘制壁纸所需的窗口了。注意WallpaperManagerService向WMS声明的Token类型为TYPE_WALLPAPER,所以, Wallpaper仅能本分地创建TYPE_WALLPAPER类型的窗口。

相应,WallpaperManagerService会在detachWallpaperLocked()函数中取消对Token的声明:

在此之后,如果这个被detach的Wallpaper想再要创建窗口便不再可能了。

WallpaperManagerService使用WindowToken对一个特定的Wallpaper做出如下限制: Wallpaper只能创建TYPE_WALLPAPER类型的窗口。 Wallpaper显示的生命周期由WallpaperManagerService牢牢地控

制着。仅有当前的Wallpaper才能创建窗口并显示内容。其他

Wallpaper由于没有有效的Token,而无法创建窗口。

InputMethod的Token的来源与Wallpaper类似,其声明位于InputMethodManager-Service的startInputInnerLocked()函数中,取消声明的位置在InputmethodManagerService的unbindCurrentMethodLocked()函数。InputMethodManagerService通过Token限制着每一个InputMethod的窗口类型以及显示生命周期。(2)Activity的Token

Activity的Token的使用方式与Wallpaper和InputMethod类似,但是其包含更多的内容。毕竟,对于Activity,无论是其组成还是操作都比Wallpaper以及InputMethod复杂得多。对此,WMS专为Activity实现了一个WindowToken的子类:AppWindowToken。

既然AppWindowToken是为Activity服务的,那么其声明自然在ActivityManagerService中。具体位置为ActivityStack.startActivityLocked(),也就是启动Activity的时候。相关代码如下:

startActivityLocked()向WMS声明r.appToken作为此Activity的Token,这个Token是在ActivityRecord的构造函数中创建的。然后在realStartActivityLocked()中将此Token交付给即将启动的Activity。

启动后的Activity即可使用此Token创建类型为TYPE_APPLICATION的窗口了。

取消Token的声明则位于ActivityStack.removeActivityFromHistoryLocked()函数中。

Activity的Token在客户端是否和Wallpaper一样,仅仅是一个基本的Binder实例呢?其实不然。看一下r.appToken的定义可以发现,这个Token的类型是IApplicationToken.Stub。其中定义了一系列和窗口相关的通知回调,它们是: windowsDrawn(),当窗口完成初次绘制后通知AMS。 windowsVisible(),当窗口可见时通知AMS。 windowsGone(),当窗口不可见时通知AMS。 keyDispatchingTimeout(),窗口没能按时完成输入事件的处理。

这个回调将会导致ANR。 getKeyDispatchingTimeout(),从AMS处获取界定ANR的时间。

AMS通过ActivityRecord表示一个Activity。而ActivityRecord的appToken在其构造函数中被创建,所以每个ActivityRecord拥有其各自的appToken。而WMS接受AMS对Token的声明,并为appToken创建了唯一的一个AppWindowToken。因此,这个类型为IApplicationToken的Binder对象appToken粘结了AMS的ActivityRecord与WMS的AppWindowToken,只要给定一个ActivityRecord,都可以通过appToken在WMS中找到一个对应的AppWindowToken,从而使得AMS拥有了操纵Activity的窗口绘制的能力。例如,当AMS认为一个Activity需要被隐藏时,以Activity对应的ActivityRecord所拥有的appToken作为参数调用WMS的setAppVisibility()函数。此函数通过appToken找到其对应的AppWindowToken,然后将属于这个Token的所有窗口隐藏。注意每当AMS因为某些原因(如启动/结束一个Activity,或将Task移到前台或后台)而调整ActivityRecord在mHistory中的顺序时,都会调用WMS相关的接口移动AppWindowToken在mAppTokens中的顺序,以保证两者的顺序一致。在后面讲解窗口排序规则时会介绍到,AppWindowToken的顺序对窗口的顺序影响非常大。4.2.2 理解WindowState

从WindowManagerService.addWindow()函数的实现中可以看出,当向WMS添加一个窗口时,WMS会为其创建一个WindowState。WindowState表示一个窗口的所有属性,所以它是WMS中事实上的窗口。这些属性将在后面遇到时再介绍。

类似于WindowToken,WindowState在显示组件一侧也有个对应的类型:IWindow.Stub。IWindow.Stub提供了很多与窗口管理相关通知的回调,例如尺寸变化、焦点变化等。

另外,从WindowManagerService.addWindow()函数中看到新的WindowState被保存到mWindowMap中,键值为IWindow的Bp端。mWindowMap是整个系统所有窗口的一个全集。注意对比一下mTokenMap和mWindowMap。这两个HashMap维护了WMS中最重要的两类数据:WindowToken及WindowState。它们的键都是IBinder,区别是:mTokenMap的键值可能是IAppWindowToken的Bp端(使用addAppToken()进行声明),或者是其他任意一个Binder的Bp端(使用addWindowToken()进行声明);而mWindowToken的键值一定是IWindow的Bp端。

关于WindowState的更多细节将在后面的讲述中进行介绍。不过经过上面的分析,不难得到WindowToken和WindowState之间的关系,参考图4-4。

更具体一些,以一个正在回放视频并弹出两个对话框的Activity为例,WindowToken与WindowState的意义如图4-5所示。图4-4WindowToken与WindowState的关系图4-5WindowState与WindowToken的从属关系4.2.3 理解DisplayContent

如果说WindowToken按照窗口之间的逻辑关系将其分组,那么DisplayContent则根据窗口的显示位置将其分组。隶属于同一个DisplayContent的窗口将会被显示在同一个屏幕中。每一个DisplayContent都对应这一个唯一的ID,在添加窗口时可以通过指定这个ID决定其将被显示在哪个屏幕中。

DisplayContent是一个非常具有隔离性的一个概念。处于不同DisplayContent的两个窗口在布局、显示顺序以及动画处理上不会产生任何耦合。因此,就这几个方面来说, DisplayContent就像一个孤岛,所有这些操作都可以在其内部独立执行。因此,这些本来属于整个WMS全局性的操作,变成了DisplayContent内部的操作。4.3 理解窗口的显示次序

在addWindow()函数的前半部分中,WMS为窗口创建了用于描述窗口状态的WindowState,接下来便会为新建的窗口确定显示次序。手机屏幕是以左上角为原点,向右为X轴方向,向下为Y轴方向的一个二维空间。为了方便管理窗口的显示次序,手机的屏幕被扩展为一个三维空间,即多定义了一个Z轴,其方向为垂直于屏幕表面指向屏幕外。多个窗口依照其前后顺序排布在这个虚拟的Z轴上,因此窗口的显示次序又称为Z序(Z order)。本节将深入探讨WMS确定窗口显示次序的过程以及其影响因素。4.3.1 主序、子序和窗口类型

看一下WindowState的构造函数:

窗口的显示次序由两个成员字段描述:主序mBaseLayer和子序mSubLayer。主序用于描述窗口及其子窗口在所有窗口中的显示位置。而子序则描述了一个子窗口在其兄弟窗口中的显示位置。 主序越大,则窗口及其子窗口的显示位置相对于其他窗口的位置

越靠前。 子序越大,则子窗口相对于其兄弟窗口的位置越靠前。对父窗口

而言,其主序取决于其类型,其子序则保持为0。而子窗口的主

序与其父窗口一样,子序则取决于其类型。从上述代码可以看到,

主序与子序的分配工作是由WindowManagerPolicy的两个成员函

数windowTypeToLayerLw()和subWindowTypeToLayerLw()完成

的。

表4-1与表4-2列出了所有可能的窗口类型以及其主序与子序的值。表4-1 窗口的主序表4-2 窗口的子序注意表4-2中的MEDIA和MEDIA_OVERLAY的子序为负值,这表明它们的显示次序位于其父窗口的后面。这两个类型的子窗口是SurfaceView控件创建的。SurfaceView被实例化后,会向WMS添加一个类型为MEDIA的子窗口,它的父窗口就是承载SurfaceView控件的窗口。这个子窗口的Surface将被用于视频回放、相机预览或游戏绘制。为了不让这个子窗口覆盖住所有的父窗口中承载的其他控件(如拍照按钮、播放器控制按钮等),它必须位于父窗口之后。

从表4-1所描述的主序与窗口类型的对应关系中可以看出,WALLPAPER类型的窗口的主序竟和APPLICATION类型的窗口主序相同,这看似有点不合常理,WALLPAPER不是应该显示在所有Acitivity之下吗?其实WALLPAPER类型的窗口是一个很不安分的角色,需要在所有的APPLICATION窗口之间跳来跳去。这是因为,有的Activity指定了android:windowShowWallpaper为true,则表示窗口要求将用户当前壁纸作为其背景。对WMS来说,最简单的办法就是将WALLPAPER窗口放置到紧邻拥有这个式样的窗口的下方。在这种需求下,为了保证主序决定窗口顺序的原则,WALLPAPER使用了与APPLICATION相同的主序。另外,输入法窗口也是一个很特殊的情况,输入法窗口会选择输入目标窗口,并将自己放置于其上。在本章中不讨论这两个特殊的例子,WALLPAPER的排序规则将在第8章中进行介绍,而输入法的排序则留给读者自行研究。

虽然知道了窗口的主序与子序是如何分配的,不过我们仍然存有疑问:如果有两个相同类型的窗口,那么它们的主序与子序岂不是完全相同?如何确定它们的显示顺序呢?事实上,表4-1和表4-2中所描述的主序和子序仅仅是排序的依据之一,WMS需要根据当前所有同类型窗口的数量为每个窗口计算最终的现实次序。4.3.2 通过主序与子序确定窗口的次序

回到WMS的addWindow()函数中,继续往下看:

这里有两个关键点: addWindowToListInOrderLocked()将新建的WindowState按照一定

的顺序插入当前DisplayContent的mWindows列表中。在分析

WMS的重要成员时提到过这个列表。它严格地按照显示顺序存

储所有窗口的WindowState。 assignLayersLocked()将根据mWindows的存储顺序对所有

WindowState的主序和子序进行调整。

接下来分别分析一下这两个函数。

1.addWindowToListInOrderLocked()分析

addWindowToListInOrderLocked()的代码很长,不过其排序原则却比较清晰。这里直接给出其处理原则,感兴趣的读者可根据这些原则自行深究相关代码。注意再次强调一下,mWindows列表是按照主序与子序的升序进行排序的,所以显示靠前的窗口放在列表靠后的位置,而显示靠后的窗口,则位于列表的前面。也就是说,列表顺序与显示顺序是相反的。这点在阅读代码时要牢记,以免混淆。在后面的叙述中,非特别强调,所谓的前后都是指显示顺序,而不是在列表的存储顺序。

子窗口的排序规则:子窗口的位置计算是相对父窗口的,并根据其子序进行排序。由于父窗口的子序为0,所以子序为负数的窗口会放置在父窗口的后面,而子序为正数的窗口会放置在父窗口的前面。如果新窗口与现有窗口子序相等,则正数子序的新窗口位于现有窗口的前面,负数子序的新窗口位于现有窗口的后面。

非子窗口的排序则是依据主序进行的,但是其规则较为复杂,分为应用窗口和非应用窗口两种情况。之所以要区别处理应用窗口,是因为所有的应用窗口的初始主序都是21000,并且应用窗口的位置应该与它所属的应用的其他窗口放在一起。例如应用A显示于应用B的后方,当应用A因为某个动作打开一个新的窗口时,新窗口应该位于应用A其他窗口的前面,但是不得覆盖应用B的窗口。只依据主序进行排序是无法实现这个管理逻辑的,还需要依赖Activity的顺序。在WindowToken一节的讲解中,曾经简单分析了mAppTokens列表的性质,它所保存的AppWindowToken的顺序与AMS中ActivityRecord的顺序时刻保持一致。因此,AppWindowToken在mAppTokens的顺序就是Activity的顺序。

非应用窗口的排序规则:依照主序进行排序,主序高者排在前面,当现有窗口的主序与新窗口相同时,新窗口位于现有窗口的前面。

应用窗口的排序规则:如上所述,同一个应用的窗口的显示位置必须相邻。如果当前应用已有窗口在显示(当前应用的窗口存储在其WindowState.appWindowToken.windows中),新窗口将插入其所属应用其他窗口的前面,但是保证STARTING_WINDOW永远位于最前方, BASE_APPLICATION永远位于最后方。如果新窗口是当前应用的第一个窗口,则参照其他应用的窗口顺序,将新窗口插入位于前面的最后一个应用的最后一个窗口的后方,或者位于后面的第一个应用的最前一个窗口的前方。如果当前没有其他应用的窗口可以参照,则直接根据主序将新窗口插入列表中。

窗口排序的总结如下: 子窗口依据子序相对于其父窗口进行排序。相同子序的窗体,正

子序则越新越靠前,负子序则越新越靠后。 应用窗口参照本应用其他窗口或相邻应用的窗口进行排序。如果

没有任何窗口可以参照,则根据主序进行排序。 非应用窗口根据主序进行排序。

经过addWindowToListInOrderLocked()函数的处理之后,当前DisplayContent的窗口列表被插入了一个新的窗口。然后等待assignLayersLocked()进一步处理。

2.assignLayersLocked分析

assignLayersLocked()函数将根据每个窗口的主序以及它们在窗口列表中的位置重新计算最终的显示次序mLayer。

[WindowManagerService.java-->WindowManagerService.assignLayersLocked()]

assignLayersLocked()的工作原理比较绕,简单来说,如果某个窗口在整个列表中拥有唯一的主序,则该主序就是其最终的显示次序。如果若干个窗口拥有相同的主序(注意,经过addWindowToListInOrderLocked()函数处理后,拥有相同主序的窗口都是相邻的),则第i个相同主序的窗口的显示次序为在主序的基础上增加i*WINDOW_LAYER_MULTIPLIER的偏移。

经过assignLayersLocked()之后,一个拥有9个窗口的系统的显示次序的信息如表4-3所示。表4-3 窗口最终的显示次序信息

在确定最终的显示次序mLayer后,又计算了WindowStateAnimator另一个属性:mAnimLayer。如下所示:

对绝大多数窗口而言,其对应的WindowStateAnimator的mAnimLayer就是mLayer。而当窗口附属为一个Activity时,mAnimLayer会加入一个来自AppWindowAnimator的矫正:animLayerAdjustment。

WindowStateAnimator和AppWindowAnimator是动画系统中的两员大将,它们负责渲染窗口动画以及最终的Surface显示次序的修改。回顾一下4.1.2节中的WMS的组成结构图,WindowState属于窗口管理体系的类,因此其所保存的mLayer的意义偏向于窗口管理。WindowStateAnimator/AppWindowAnimator则是动画体系的类,其mAnimLayer的意义偏向于动画,而且由于动画系统维护着窗口的Surface,因此mAnimLayer是Surface的实际显示次序。

在没有动画的情况下,mAnimLayer与mLayer是相等的,而当窗口附属为一个Activity时,则会根据AppTokenAnimator的需要适当地增加一个矫正值。这个矫正值来自AppTokenAnimator所使用的Animation。当Animation要求动画对象的ZOrder必须位于其他对象之上时(Animation.getZAdjustment()的返回值为Animation.ZORDER_TOP),这个矫正是一个正数WindowManagerService.TYPE_LAYER_OFFSET(1000),这个矫正值很大,于是窗口在动画过程中会显示在其他同主序的窗口之上。相反,如果要求ZOrder必须位于其他对象之下时,矫正为-WindowManagerService.TYPE_LAYER_OFFSET(-1000),于是窗口会显示在其他同主序的窗口之下。在动画完结后,mAnimLayer会被重新赋值为WindowState.mLayer,使得窗口回到其应有的位置。

动画系统的工作原理将在4.5节详细探讨。注意矫正值为常数1000,也就出现一个隐藏的bug:当同主序的窗口的数量大于200时, APPLICATION窗口的mLayer值可能超过22000。此时,在对mLayer值为21000的窗口应用矫正后,仍然无法保证动画窗口位于同主序的窗口之上。不过超过200个应用窗口的情况非常少见,而且仅在动画过程中才会出现bug,所以Google貌似也懒得解决这个问题。4.3.3 更新显示次序到Surface

再回到WMS的addWindow()函数中,发现再没有可能和显示次序相关的代码了。mAnimLayer是如何发挥自己的作用呢?不要着急,事实上,新建的窗口目前尚无Surface。回顾一下SimpleWindow例子,在执行session.relayout()后,WMS才为新窗口分配了一块Surface。也就是说,只有执行relayout()之后才会为新窗口的Surface设置新的显示次序。

为了不中断对显示次序的调查进展,就直接开门见山地告诉大家,设置显示次序到Surface的代码位于WindowStateAnimator.prepareSurfaceLocked()函数中,是通过Surface. setLayer()完成的。在4.5节会为大家深入揭开WMS动画子系统的面纱。4.3.4 关于显示次序的小结

这一节讨论了窗口类型对窗口显示次序的影响。窗口根据自己的类型得出其主序及子序,然后addWindowToListInOrderLocked()根据主序、子序以及其所属的Activity的顺序,按照升序排列在DisplayContent的mWindows列表中。然后assignLayersLocked()为mWindows中的所有窗口分配最终的显示次序。之后,WMS的动画系统将最终的显示次序通过Surface. setLayer()设置进SurfaceFlinger。4.4 窗口的布局

在本节中将讨论WMS如何对窗口进行布局。窗口布局是WMS的一项重要工作内容,而且其过程非常复杂,所以这将是本章中必须啃也是最难啃的一块骨头。回顾一下SampleWindow这个例子,将窗口添加到WMS后,需要执行IWindowSession.relayout()函数后才能获得一块可供作画的Surface,并将其放置在SampleWindow例子所指定的位置上。简单来说,IWindowSession.relayout()函数的作用就在于根据客户端提供的布局参数(LayoutParameters)为窗口重建Surface(如果有必要),并将其放置在屏幕的指定位置。同IWindowSession.add()函数一样,IWindowSession.relayoutWindow()函数对应于WMS的relayoutWindow()函数。

事实上,relayoutWindow()函数并不是本节的重点,它是用来引出WMS中最重要也是最复杂的一个函数—performLayoutAndPlaceSurfacesLocked()。relayoutWindow()函数修改指定窗口的布局参数,然后performLayoutAndPlaceSurfacesLocked()遍历所有窗口并对它们进行重新布局。这种牵一发而可能动全身的做法看似效率低下,但是确实很有必要,因为多个窗口之间的布局是相互影响的。

另外,通过这一节的学习与分析,读者将加深对WindowState和WindowToken这两个概念的认识。

再正式开始本节的内容之前,先看一下WMS窗口属性更新的总体过程,如图4-6所示。图4-6 属性更新的总体过程

图4-6揭示了WMS布局三步走的流程。即将讨论的relayoutWindow()函数属于第一步,除此之外,屏幕旋转等操作也属于第一步的范畴。第二步的布局“子系统”这个说法其实并不太准确,因为这个所谓的子系统只是以performLayoutAndPlaceSurfaceLocked()函数为主入口的一组函数的集合。而动画子系统则是WMS中由Choreographer驱动的一系列的WindowAnimator,这一部分将在后续内容中介绍。

接下来,就从relayoutWindow()开始,揭开WMS窗口布局管理的面纱吧。4.4.1 从relayoutWindow()开始

首先我们要理解relayoutWindow()的参数的意义。WMS的relayoutWindow()的签名如下:

参数很多,从名字上可以看出有些参数是返回给调用者的。下面介绍它们的意义。 session:调用者所在进程的Session实例。 client:需要进行relayout的窗口。 seq:一个和状态栏/导航栏可见性相关的序列号,在第6章再进

行探讨。 attrs:窗口的新布局属性。relayoutWindow()的主要目的就是根

据attrs所提供的布局参数重新布局一个窗口。客户端可以通过

relayoutWindow()函数改变attrs中所定义的几乎所有布局属性。

但是唯独无法改变窗口类型。 requestedWidth与requestedHeight:客户端所要求的窗口尺寸。

在重新布局的过程中, WMS会尽量将窗口的尺寸布局为客户端

所要求的大小。 viewVisiblility:窗口的可见性。 flags:定义一些布局行为。 outFrame:由relayoutWindow()函数返回给调用者的一个Rect类

型的实例。它保存了窗口被重新布局后的位置与大小。 outContentInsets与outVisibleInsets:这两个参数表示了窗口可以

绘制内容的矩形边界与可视矩形边界在四个方向上到mFrame的

像素差。 outConfiguration:重新布局后,WMS为此窗口计算出的

Configuration。 outSurface: 用来接收WMS为此窗口分配的Surface。窗口的第一

次relayout完成后就可以通过它在窗口中进行绘图了。卷I的

Surface系统一章中曾经介绍了这个参数的具体细节。本节不再

赘述。注意relayoutWindow()基本上可以为窗口更新所有在LayoutParameter类中定义的属性,但是窗口类型是一个例外。窗口类型必须在添加窗口时加以指定,并且不允许再做更改。

这段代码稍稍有点长,下面总结一下它的工作内容: 根据参数更新窗口的WindowState对象的相应属性,这些属性是

后续对其进行布局的依据。 处理窗口的显示与退出。这里主要是一些涉及Surface的创建/

销毁,以及动画相关的操作。这不是本节讨论的重点。 更新和窗口相关的其他机制,例如焦点、输入法、壁纸以及屏幕

旋转等。 调用performLayoutAndPlaceSurfaceLocked()函数进行布局。 将布局结果返回给relayoutWindow()的返回者。

之所以使用relayoutWindow()作为布局的切入点,一是因为它是WMS提供的最常用的API之一,二是它体现了WMS窗口相关操作的一个通用做法: 修改一些属性(窗口属性、屏幕属性、焦点属性等)。 标记相关的DisplayContent为relayoutNeeded。调用

performLayoutAndPlaceSurfaceLoc ked()进行全局布局。 对布局结果进行进一步的操作。

通过这个通用做法可以对performLayoutAndPlaceSurfaceLocked()函数拥有一个感性认识。简单地说,窗口的属性可以分为两类:一类是布局控制属性,例如mRequestedWidth/Height、mAttrs等,由客户端根据需要进行设置,用来向WMS表达它所期望的窗口布局;另一类是布局结果属性,例如mFrame、mContentInsets、mVisibleInsets等,它们是经过布局过程计算出来的,直接影响到窗口的实际布局与显示,客户端无法直接干预这些属性,只能被迫接受这些布局结果。performLayoutAndPlaceSurfaceLocked()函数就是通过布局控制属性计算布局结果属性这一过程的场所。提示客户端只能被迫接受窗口的布局结果,是因为客户端调用relayoutWindow()时所要求的尺寸和位置参数和最终的布局结果有出入。研究一下Android控件系统的核心类ViewRootImpl的实现就可以发现,它作为控件系统到窗口系统的桥梁,采用了协商式的函数来绘制控件。在窗口显示之前,ViewRootImpl首先对整个控件树进行尺寸测量,得到一个理想尺寸,并将这个理想尺寸作为relayoutWindow()的requestedWidth/Height参数交给WMS进行布局。布局之后,ViewRootImpl会根据WMS的实际布局结果(frame,contentInsets,visibleInsets)重新对控件树进行一次测量,得到最终尺寸,然后进行绘制。有兴趣的读者可以做一个试验,在relayoutWindow()函数最后修改outFrame的bottom为原来的1/2,看一下是什么样的结果。4.4.2 布局操作的外围代码分析

接下来开始剖析WMS的布局过程的征途吧。当然是从performLayoutAndPlaceSurfacesLo-cked()开始。

实现比较简单,很明显可以看出,此函数中有意义的事情都是在performLayoutAndPlace-SurfacesLockedLoop()中完成的。

虽然这个函数只有短短的7行,但是却包含了一个乍看上去有点不明所以的do-while循环。它的循环条件是mTraversalScheduled为true并且loopCount大于0。为了搞清楚这个循环的目的所在,首先要找到mTraversalScheduled变量的意义何在。原来,修改这个变量的只有WMS为外界提供的一个名为requestTraversal()的接口函数。其实现如下:

这个实现自WindowManagerFuncs接口的函数向外界提供了一个触发WMS立刻进行重新布局的手段。注意,requestTraversalLocked()是在被mWindowMap这个锁的保护下调用的,这与performLayoutAndPlaceSurfaceLocked()是一样的。也就是说外部线程不可能影响这个循环的判定条件。那就只剩下了一个可能:performLayoutAndPlaceSurface sLockedLoop()函数中会调用requestTraversalLocked()。现在就到这个函数内部去求证一下吧。

这个函数仍不算复杂。主要的布局逻辑应该位于performLayoutAndPlaceSurfacesLockedIn-ner()中。需要注意的地方应该就是requestTraversalLocked()的两个调用条件了。

第一个条件是needsLayout()函数的返回值。还记得在分析relayoutWindow()时所提到的DisplayContent.mLayoutNeeded字段吗?needsLayout()函数遍历所有DisplayContent并检查它们的mLayoutNeeded字段的值。只要有一个DisplayContent需要重新布局,此函数就会返回true。这说明一个问题,就像罗马不是一天建成的一样,布局也不是一遍就能完成的!

至于第二个条件,mLayoutRepeatCount<6,则表示最多连续重新布局6次,这和performLayoutAndPlaceSurfaceLocked()中的循环条件有所重复。这说明了一个更严重的问题,布局有可能重复6次也无法完成,为了防止调用者过度等待,以致需要增加一个6次的限制。

到这里,我们对布局的外围有了一个简单认识,如图4-7所示。图4-7 布局的总体流程4.4.3 初探performLayoutAndPlaceSurfacesLockedInner()

接下来要继续深入WMS的布局过程。performLayoutAndPlaceSu-rfacesLockedInner()函数不只名字长,其实现更长,有近600行之多。而且Android 4.2已经通过提炼一部分代码到独立的函数中为其减肥。为了防止在研究这个函数的过程中迷路,先向大家提供将这个函数提炼后的伪代码:

可以看出,这个函数的整体逻辑是比较清晰的。不过由于它的全能性,每一个步骤的内容都相当繁杂。这一点从其注释中可以看出:Something has changed! Let’s make it correct now!不用担心,我们将一步一个脚印地探索这个函数的实现。4.4.4 布局的前期处理

在布局的前期,主要工作有: 如果有必要,计算新的焦点窗口。第一次见到

updateFocusedWindowLocked()函数是在relayoutWindow()中,如

果客户端要求relayout的窗口的可见性发生变化,则重新计算焦

点窗口。 初始化mInnerFields中的一部分字段,mInnerFields是

LayoutFields类的一个实例。事实上,LayoutFields类本身并没有

什么实际意义,不过是把一些布局相关的状态变量组合到一起而

已。在后面使用时再对其进行说明。 递增布局序号mTransactionSequence。WMS每进行一次布局都

会导致序号递增。AppWindowToken中也保存了一个相应的布局

序号,在布局的过程中,WMS通过对比这两个序号的值以确定

AppWindowToken的布局状态是否最新。 布局水印和StrictMode警告框。水印用以在屏幕上显示一段固定

的信息,而StrictMode警告框则在任何一个应用或服务发生违例

操作时在屏幕上闪烁一个红色方框。它们其实是两块Surface,

而它们的显示次序分别为1000000和1000001,高于任何一个窗

口的显示次序,所以它们将显示在所有窗口之上。它们的布局非

常简单:占据整个显示画面。

布局的前期处理看起来没有什么复杂的。接下来开始对每个DisplayContent进行布局。4.4.5 布局DisplayContent

需要再次强调一下,Android 4.2之后将支持多块屏幕输出(目前第二块屏幕是WiFi-Display设备)。而一个DisplayConent则用来描述一块屏幕。在performLayoutAndPlaceSurfac-esLocked()函数的后续代码中,将遍历系统中所有的DisplayContent,并分别对它们各自所拥有的窗口进行布局。这一部分内容是performLayoutAndPlaceSurfacesLockedInner()函数的核心所在。

整理一下目前的思路。布局DisplayContent整体上分为两个部分: 布局循环。这个do-while循环的主要工作是对DisplayContent所拥

有的窗口进行布局,其工作侧重于PerformLayout。 布局后处理。后处理用于根据布局结果设置Surface参数,应用

一些动画效果等,其工作侧重于PlaceSurfaces。

1.深入理解窗口布局的原理

接着上面的讨论,深入研究第一部分的布局循环来探究WMS是如何完成窗口布局的。仔细归纳一下其循环体,可以发现其明显分为三个阶段: pendingLayoutChanges处理阶段。这个阶段先不讨论。当学习了

结果检查阶段后再回头探讨这个阶段。 布局阶段。主要内容就是performLayoutLockedInner()函数。这个

函数将对Display-Content下的所有窗口进行布局。 结果检查阶段。在所有窗口的布局完成后,通过一些状态量的检

查,决定是否重做这三个阶段的工作。其检查内容主要是状态栏、

导航栏可见性是否与顶层窗口的属性冲突,是否需要解除锁屏状

态等。

这个循环不断地对DisplayContent中的所有窗口进行布局操作,并处理pending-LayoutChanges,直到WMP的finishPostLayoutLw()认为当前布局不需要再做任何改动。如果在循环4次之后仍无法满足finishPostLayoutLw()的要求,则不再尝试对窗口进行布局,仅处理pendingLayoutChanges。如果6次尝试之后仍然无法满足要求,则放弃继续尝试。

接下来先深入学习布局阶段的工作原理。通过这一节的学习,希望读者能够了解WMS如何计算每一个窗口的位置与尺寸。(1)初识performLayoutLockedInner()

我们首先来看performLayoutLockedInner()是如何对窗口完成布局的。参考其代码:

可以看出,窗口布局过程的规律性还是很强的。 在进行布局前,首先通过执行WindowManagerPolicy.

beginLayoutLw()通知WMP为即将开始的布局进行准备。这个准

备过程就是通过屏幕的尺寸、状态栏/导航栏的可见性、屏幕旋

转状态等因素来计算布局所使用的参数。 首先对所有顶级窗口进行布局。这个布局过程也分为两步,分别

为WindowState. prelayout()和WindowManagerPolicy.

layoutWindowLw()。其工作就是使用上一步所计算出的布局参数

计算出窗口的位置属性,并保存在WindowState中。 之后对所有子窗口进行布局,其布局过程与顶级窗口一致。之所

以要将顶级窗口与子窗口分开进行布局,是因为子窗口的布局依

赖于其父窗口的布局结果。 最后,执行WindowManagerPolicy.finishLayoutLw()。告知WMP

本次布局已完成,可以清理布局过程中所使用过的资源。

可以看出,在窗口布局过程中,WindowManagerPolicy发挥了近乎决定性的作用。在Android 4.2中,WindowManagerPolicy是由PhoneWindowManager实现的,所以上述的关键函数将以PhoneWindowManager的实现为基础进行讨论。

接下来将深入研究这三个阶段的工作原理。(2)窗口布局的8个准绳

在PhoneWindowManager.beginLayoutLw()中究竟为布局准备了什么参数呢?简单来说,这些参数描述了屏幕上的8个矩形区域,而这8个区域构成了PhoneWindowManager布局窗口的准绳。由于beginLayoutLw()函数内容相对琐碎,因此下面直接给描述这8个矩形区域的参数的意义以及其计算依据。建议感兴趣的读者在读完本节后可以仔细研究一下这个函数,以加深理解。 mUnrestrictedScreenLeft/Top/Width/Height:描述了整个屏幕的

逻辑显示区域,也就是(0,0-dw,dh)。 mRestrictedScreenLeft/Top/Width/Height: 描述了屏幕中导航栏之

后的区域。这个区域会受到导航栏的可见性影响。当导航栏不可

见时,这块区域与Unrestricted区域等价。

UnrestrictedScreen区域与Restricted区域在窗口布局的过程中常常被用作布局容器。布局容器可以用来应用窗口的对齐方式,或者对窗口的尺寸及位置加以限制。 mStableFullscreenLeft/Top/Right/Bottom:描述了整个屏幕的逻

辑显示区域,和Unrestricted区域等价。 mStableLeft/Top/Right/Bottom:描述了屏幕排除状态栏和导航栏

之后的区域。这个区域并不受状态栏与导航栏的可见性影响。即

便状态栏或导航栏被隐藏,Stable区域仍然为排除状态栏与导航

栏之后的区域。这也是Stable这个名称的由来。

StableFullscreen区域和Stable区域并不直接参与窗口的布局过程,而是为WMS的客户端提供一个不受状态栏/导航条的可见性所影响的显示区域大小及位置。 mDockLeft/Top/Right/Bottom:Dock区域用来描述可用来放置停

靠窗口的区域。所谓的停靠窗口是指显示在屏幕某一侧的半屏窗

口,例如输入法窗口。Dock区域最主要的用途就是用来作为输

入法窗口的布局容器。 mContentLeft/Top/Right/Bottom:描述屏幕中排除了状态栏、导

航栏以及输入法(如果显示)后的屏幕区域。与Restricted区域

相比,Content区域额外地受到了状态栏及输入法的影响。当没

有状态栏及输入法显示时,Content区域与Restricted区域是等价

的。 mCurLeft/Top/Right/Bottom:与Content区域一样,它也描述了屏

幕中排除了状态栏、导航栏以及输入法(如果显示的话)后的屏

幕区域。在大部分情况下,Cur区域与Content区域是相同的。但

是这两个区域的计算依据不同。

Content区域和Cur区域与其他区域有个不同的地方:它们的区域并不是在begin-LayoutLw()中被最终确立下来。因为它们受输入法窗口的尺寸影响,所以必须在输入法窗口完成布局后,在offsetInputMethodWindowLw()函数中得到最终的位置。而在此之前,它们与Dock区域是相同的。 mSystemLeft/Top/Right/Bottom:与Dock区域的绝大多数情况是

相同的,但它们有一个很微妙的不同。状态栏与导航栏的可见性

发生变化时有一个淡入淡出的动画效果,这两个区域在这个淡入

淡出的过程中是不一致的。Dock认为在淡入淡出的动画过程

中,状态栏与导航栏仍然是可见的,而System区域则认为它们

是不可见的。图4-8与图4-9较为直观地描述了布局参数的具体位

置。图4-8 竖屏下布局参数所指示的位置图4-9 横屏下布局参数所指示的位置

一旦这8个矩形区域确定下来,PhoneWindowManager就准备好对窗口进行布局了。提示读者可以通过执行adb shell dumpsys window命令来查看这8个区域的当前取值。它们显示在WINDOWMANAGERPOLICYSTATE一栏中。(3)窗口布局的4个参数

上一小节讨论了PhoneWindowManager的beginLayoutLw()所准备好的布局参数。接下来讨论如何通过这些布局参数确定一个窗口的位置和尺寸。

如前所述,布局窗口使用了两个函数,分别是WindowState.prelayout()和WindowManager-Policy.layoutWindowLw()。prelayout()函数初始化了一个尺寸放大系数,用于在兼容模式下显示窗口,在正常情况下,此放大系数保持为1,本章不准备讨论这个问题,所以,直接看一下PhoneWindowManager的layoutWindowLw()的实现:

[PhoneWindowManager.java-->PhoneWindowManager.layoutWindowLw()]

layoutWindowLw()首先计算了pf、df、cf和vf这4个矩形,然后将这4个矩形作为参数调用WindowState的computFrameLw()函数完成布局,这4个矩形便成为窗口布局的4个关键参数。 ParentFrame(pf):描述了放置窗口的容器的位置与尺寸。通过

LayoutParam为窗口指定的布局参数如x、y、width、height及

gravity等都是相对于pf进行计算的。也就是说, pf对窗口的位置

与尺寸计算的影响在4个矩形中起到了决定作用。 DisplayFrame(df):df用来限制窗口的最终位置。当窗口通过

ParentFrame完成位置与尺寸的计算后,需要通过df再进行一次

校正,要求窗口必须完全位于df之内。 ContentFrame(cf):cf不会直接影响窗口布局位置与尺寸,但是

它影响了窗口内容的绘制。cf表示当前屏幕上排除所有系统窗口(状态栏、导航栏以及输入法)后所留下的矩形区域。 VisibleFrame(vf):和cf一样,vf也不会直接影响窗口布局的位置

与尺寸,而是会影响窗口内容的绘制。vf表示在当前屏幕上,完

全不被任何系统窗口所遮挡的一块矩形区域。

那么,这4个参数与前面一节所提到的8个准绳是什么关系呢?简单分析一下layoutWindowLw()计算这4个参数的代码便可知道,这4个参数是layoutWindowLw()根据需要从8个准绳当中选出来的。例如,根据窗口的类型、flag的不同,pf可能是Restricted、Unrestricted、Dock或Content区域。df则在绝大部分情况下与pf的取值保持着一致。cf则根据LayoutParams.softInputMode以及flag的取值不同而可能是Content、Dock或Restricted区域。vf则根据LayoutParams.softInputMode的取值选择与cf保持一致,或者选择Cur区域。另外,对于子窗口,其4个参数的取值依赖于其父窗口。

由于需要考虑到的情况非常多,因此layoutWindowLw()花了很大篇幅计算这4个参数。虽然繁琐却并不复杂,所以这里就不贴出来了,读者可以根据自己的实际需要去探索这段代码。

了解computeFrameLw()的参数之后,接下来就要研究computeFrameLw()的工作原理,以及经过这么复杂的流程,对一个窗口进行布局后的产出是什么。(4)窗口布局的产出物

参考computeFrameLw()的代码:

整理一下computFrameLw()的产出。 mFrame:描述了窗口的位置与尺寸。 mContainerFrame与mParentFrame:这两个矩形是相等的,保存

了pf参数。 mDisplayFrame:保存了df参数。 mContentFrame与mContentInsets:mContentFrame表示了在当

前窗口中可以用来显示内容的区域,由cf与mFrame相交得出。

而mContentInsets则表示了mContentFrame与mFrame的4条边界

之间的距离。relayoutWindow()函数有一个输出参数

outConentInsets,即mContentInsets。 mVisibleFrame与mVisibleInsets:mVisibleFrame表示在当前窗口

中不被系统窗口所遮挡的区域,由vf与mFrame相交得出。

mVisibleInsets同mContentInsets一样,表示mVisibleFrame与

mFrame的4条边界之间的距离。relayoutWindow()函数的另一个

输出参数outVisibleInsets就是mVisibleInsets。客户端可以通过

outVisibleInsets的取值,适当地滚动自己的显示,以确保一些重

要信息不被遮挡。

至此,单个窗口的布局过程便完成了。在经历布局之后,窗口的位置尺寸、内容区域的位置尺寸、可视区域的位置尺寸都得到了更新。这些更新将会影响到窗口Surface的位置尺寸,并且还会以回调的方式通知窗口的客户端,进而影响窗口内容的绘制。

回到performLayoutLockedInner()函数,在DisplayContent下的所有窗口都已完成布局之后,则调用WindowManagerPolicy.finishLayoutLw()函数,要求WMP释放布局过程中所使用的资源,或者做一些策略处理。不过WMP的实现者PhoneWindowManager在这个函数中并没有做任何事情。(5)关于窗口布局的小结

到此为止,performLayoutLockedInner()函数的整个工作流程的分析便完成了。perform-LayoutLockedInner()对给定的DisplayContent下的所有窗口进行布局,使窗口的mFrame等布局变量的值为最新,以便在后续工作中根据这些布局变量的取值来放置各个窗口的Surface,应用一些显示效果,以及影响客户端显示内容的绘制。performLayoutLockedInner()函数的主要工作流程如图4-10所示。图4-10 performLayoutLockedInner()的工作流程

窗口布局的演算过程为:根据DisplayContent的属性,以及状态栏、导航栏及输入法窗口的状态,确定8个布局区域。然后根据每个窗口的状态,从8个布局区域中选出4个布局参数pf、df、cf以及vf(可以重复选择)。之后,窗口根据这4个参数计算出布局结果并保存在WindowState中。

2.检查窗口布局结果

上一节所探讨的内容主要围绕在窗口位置尺寸的布局计算上。经过perform-LayoutLockedInner()之后,窗口已经各就各位。然而,一些窗口的flag对布局系统还有更多的要求,例如: FLAG_FORCE_NOT_FULLSCREEN,拥有这个flag的窗口被显

示时,必须同时显示状态栏等系统窗口。 FLAG_FULLSCREEN,拥有这个flag的满屏窗口被显示时,必须

隐藏状态栏等系统窗口。注意,这个flag与

FLAG_FORCE_NOT_FULLSCREEN是冲突的。如果两个窗口分

别指定了这两个flag,那么FLAG_FORCE_NOT_FULLSCREEN

的优先级要高。FLAG_FULLSCREEN将被忽略。 SYSTEM_UI_FLAG_FULLSCREEN,这是通过

view.setSystemUIVisibility()设置的一个flag。其作用与

FLAG_FULLSCREEN一样。 FLAG_SHOW_WHEN_LOCKED,在锁屏情况下,拥有这个flag

的窗口被显示时,隐藏锁屏界面,以使窗口得以呈现给用户。当

窗口关闭时,自动恢复锁屏。由于这个flag无视用户设置的加密

解锁,因此出于安全考虑,这个flag仅对能够充满屏幕(不包括

状态栏等系统窗口)的窗口起作用。 FLAG_DISMISS_KEYGUARD,与

FLAG_SHOW_WHEN_LOCKED类似,拥有这个flag的窗口被显

示时,将会退出锁屏状态,而且窗口关闭时不会再恢复锁屏。不

过此flag的作用也有限制,如果用户设置了密码、图形等安全解

锁措施,则在用户输入解锁密码或图形之前,此窗口不会被显示。

这个flag仅对能够充满屏幕(不包括状态栏等系统窗口)的窗口

起作用。

由于这些flag影响了系统窗口、锁屏界面的可见性,也就会影响窗口的布局过程。然而让人头痛的是,这些flag的生效条件是要求窗口能够覆盖整个屏幕,因此窗口的布局也影响了这些flag的有效性。这两方面互相依赖又互相影响,因此,WMS引入了pendingLayoutChanges机制。

这个机制的原理是:首先依据当前的系统窗口、锁屏界面的有效性进行布局,然后基于这个布局来生效上述flag,并检查系统窗口、锁屏界面的可见性是否发生了变化。如果可见性发生变化,则通过pendingLayoutChanges变量标记下需要额外完成何种工作。在完成这些工作之后,重新进行布局,再进行检查,如此反复,直到系统窗口与锁屏界面的可见性不再发生变化为止。

接下来将分析pendingLayoutChanges的工作机制。回顾一下performLayoutAndPlaceSurfa-cesLockedInner()函数中的那个布局循环。

很显然,我们的调查重点在于WMP的三个函数上。(1)beginPostLayoutPolicyLw()分析

beginPostLayoutPolicyLw()在PhoneWindowManager中的实现比预期的要简单很多,仅仅初始化了一些状态变量。而且方便将布局结果检查过程中所需要的参数一网打尽。

相信读者应该能想到三个函数的作用了:beginXXX()用来初始化参数,applyXXX()用来设置上述参数,而finishXXX()则用来根据上述参数执行相应的动作。(2)applyPostLayoutPolicyLw()分析

在遍历所有的窗口之后,beginPostLayoutPolicyLw()中遇到的状态变量都已被设置了合适的值。对我们的分析目标来说,比较重要的状态变量有: mTopFullscreenOpaqueWindowState,类型是WindowState,保

存了第一个满屏窗口。这个满屏窗口的FLAG_FULLSCREEN将

会导致系统窗口被隐藏。 mForceStatusBar和mForceStatusBarFromKeyguard,这两个状态

变量都会要求强制显示系统窗口,对应于

FLAG_FORCE_NOT_FULLSCREEN。不同的地方仅仅在于带有

这个flag的窗口的类型是否是TYPE_KEYGUARD。 mHideLockScreen,要求隐藏锁屏界面,对应于

FLAG_SHOW_WHEN_LOCKED。从代码中可以看出,仅当第一

个满屏显示的应用窗口拥有这个flag时才会被置为true。 mDismissKeyguard,表示是否执行解锁。这是一个int型的变

量,取值为DISMISS_KEYGUARD_NONE/START/CONTINUE三

种。NONE表示不会解锁,START表示执行解锁动作,而

CONTINUE则表示维持当前状态。(3)finishPostLayoutPolicyLw()分析

接下来,finishPostLayoutPolicyLw()将会根据上述状态变量,调整系统窗口与锁屏界面的可见性,因为逻辑非常简单直接,这里就不贴代码了。

需要提醒的是,FLAG_FULLSCREEN和SYSTEM_UI_FLAG_FULLSCREEN这两个flag并没有相应的状态变量与之对应。在finishPostLayoutPolicyLw()函数中,当mForceStatusBar及mForceStatusBarFromKeyguard都为false时,会从mTopFullscreenOpaqueWindowState以及mLastSystemUIVisiblity中查找这两个flag,并尝试隐藏系统窗口。

finishPostLayoutPolicyLw()的另外一个工作就是确定pendingLayoutChanges的值。当系统窗口的可见性发生变化时,仅会影响窗口的尺寸,因此在这种情况下pending-LayoutChanges的值为FINISH_LAYOUT_REDO_LAYOUT。要求performLayoutLockedInner()函数在新的系统窗口可见性下重新对所有窗口进行布局。

而当锁屏窗口的可见性发生变化时,情形就比较复杂了,因为当锁屏窗口被隐藏时,显示给用户的窗口所要求的屏幕旋转方向与锁屏窗口可能不一致,这就需要WMS重新计算屏幕方向。另外,显示给用户的窗口可能要求系统壁纸作为其背景,所以WMS还需要重新调整壁纸显示次序,而且无论如何,重新对所有窗口进行布局也是难免的。因此,在锁屏窗口的可见性发生变化后,pendingLayoutChanges的值为FINISH_LAYOUT_REDO_LAYOUT|FINISH_LAYOUT_REDO_WALLPAPER|FINISH_LAYOUT_REDO_CONFIG。

当然,如果两者的可见性都没有发生变化,pendingLayoutChanges的值为0。

3.处理pendingLayoutChanges

窗口布局完成了,布局检查也完成了,pendingLayoutChanges也被设置为合适的值。接下来回过头再看performLayoutAndPlaceSurfacesLockedInner()的那个布局循环中处理pendingLayoutChanges的代码:

至此本节分析了DisplayContent布局过程中的布局循环部分的工作原理。

4.DisplayContent的布局后处理

在布局过程几经周折离开布局循环后,窗口的位置与尺寸都已经确定。不过,确定的仅仅是每个窗口的尺寸和位置信息,窗口Surface尚未更新。也就是说,到目前为止,用户还没有看到窗口被放置到布局过程中所指定的位置。

在接下来的布局后处理中,performLayoutAndPlaceSurfacesLockedInner()将会为布局好的窗口设置其Surface大小与尺寸,并附加一些动画效果,例如弹出对话框后的变暗效果(Dimming)等。另外,在布局的前期处理中初始化了mInnerFileds对象所保存的一些手机状态,现在是时候更新它们了。看一下DisplayContent的布局后处理的代码:

DisplayContent的布局后处理的主要工作内容是: 设置窗口的遮挡状态。 从窗口的LayoutParams中提取关于屏幕亮度、键盘亮度、输入

超时等设置。 发起或取消Dimming效果。 设置窗口Surface的位置与尺寸。其中,位置的变化是有动画效

果的,而WMS出于性能考虑,尺寸的变化则没有动画效果。 如果窗口的客户端已经完成对Surface的绘制工作,则显示这个

窗口。

完成布局后处理后,当前DisplayContent的布局就完成了,所有的窗口都已各就各位。

5.关于DisplayContent布局的小结

经历了漫长的代码分析,DisplayContent布局过程终于完结了。再次回顾一下布局过程中的两大阶段: 以窗口布局计算为主要任务的布局循环。布局循环以

performLayoutLockedInner()函数为核心,根据屏幕尺寸以及状态

栏、导航栏、输入法窗口等系统窗口确定了作为窗口布局准绳的

8个矩形,再从8个矩形中选择4个矩形作为窗口布局的直接参数,

并以这4个参数计算出窗口的最终位置。 根据布局计算结果设置Surface的位置与尺寸,并更新一些由窗

口指定的系统属性为目的布局后处理。

DisplayContent的布局其实就是窗口的布局,而窗口的布局中又以窗口的布局计算为核心。这部分内容需要读者尽可能深刻地理解。4.4.6 布局的最终阶段

在布局的最终阶段中,所有的DisplayContent都已基本完成布局(之所以说基本完成,是因为如果布局循环的次数大于6次,有可能DisplayContent.pendingLayoutChanges仍然不为0)。

布局的最终阶段的工作相对繁杂。包括设置屏幕亮度、背光亮度、保持屏幕唤醒等(基于布局后处理时设置的mInnerFields中的相关状态),通知窗口客户端其布局发生变化等。在这里不再一一说明。

其中比较重要的是,在DisplayContent的布局循环中有可能没能将pendingLayoutChanges清零,此时需要设置DisplayContent的layoutNeeded为true,由布局外围循环重新进行一遍完整布局。

另外,在设置窗口的位置时使用了动画,并且Dimming效果也是基于动画实现的,因此在布局的最后,需要通过调用updateLayoutToAnimationLocked()启动动画系统。4.5 WMS的动画系统

在本章前面的讨论中,曾经多次看到WindowStateAnimator的身影:在perpare-SurfaceLocked()中设置Surface的显示次序。在relayoutWindow()中创建及销毁Surface,在performLayoutAndPlaceSurfacesLockedInner()中设置Surface的尺寸与位置,等等。WindowStateAnimator在Surface的操作过程中发挥了极大的作用,而且布局过程中还有一些和动画系统相关的重要的内容尚未解释,例如mShownFrame的计算、窗口绘制的状态机的原理等,这一节将深入探讨WMS的动画系统。注意这里再强调一下,WMS不负责窗口的具体绘制,因此WMS动画系统仅会影响窗口的位置、显示尺寸与透明度,不会影响窗口上所绘制的内容。窗口内容的动画在窗口的客户端由ViewRootImpl驱动完成。另外,动画系统中所改变的显示尺寸与布局过程中的Surface尺寸是两个不同的概念。布局过程中的Surface尺寸是Surface的实际尺寸,这个尺寸决定了其GraphicBuffer的大小以及Canvas可以绘制的区域。而动画过程中的尺寸则是渲染尺寸,只是在最终输出的过程中将Surface的内容放大或缩小。4.5.1 Android动画原理简介

在正式开始WMS动画系统的探讨之前,有必要先了解一下Android动画的工作原理。

1.Animation类与Transform类

Android提供了平移(Translate)、缩放(Scale)、旋转(Rotate)以及透明度(Alpha)4种类型的动画。这些动画分别由TranslateAnimation类、ScaleAnimation类、RotateAnimation类以及AlphaAnimation类实现,它们都是Animation类的子类。

直观上,很多人可能都会认为Animation及其子类在开始动画后会在某一个线程中以一定的频率不断地操作动画目标的相关属性,从而实现动画效果。其实不然,Android中的Animation类的功能非常轻量级。在给定了初始状态、结束状态、启动时间与持续时间后,该类可以为使用者计算其动画目标在任意时刻的变换(Transformation),这是Animation类唯一的用途。

Transformation类描述了一个变换。它包含了两个分量:透明度及一个二维变换矩阵。同变换矩阵的运算一样,多个Transformation也可以进行类似于矩阵的先乘、后乘等叠加操作。其计算方法为,透明度分量相乘,变换矩阵分量进行相应的先乘、后乘。说明二维变换矩阵可以在二维空间中对变换目标实现平移、旋转、缩放与切变等效果。将两个矩阵相乘可以组合其各自的变换效果。关于其变换的原理与运算,有很多图形学相关的书籍与文章可以参考,本书不再赘述。

本节将探讨一下Animation类是如何计算Transformation的。获取Transformation使用Animation类的getTranformation()函数,这个函数根据给定的时间戳,计算动画目标所需要进行的变换。另外,其返回值如果为true,则表示动画尚未完结,调用者需要继续处理下一帧动画。

在真正的变换计算开始之前,Animation首先将currentTime进行一个“标准化”操作。因为动画的时间参数有起始时间、滞后时间、持续时间等。其中滞后时间是指从起始时间开始,到真正开始执行动画的时间间隔,滞后时间往往用于两个动画的衔接。倘若让这些时间参数参与到变换计算中无疑会大大增加计算的复杂度。“标准化”正是为了解决这个问题。

normalizedTime就是currentTime经过标准化后的时间,我们称为标准时间。标准时间隐藏了上述几个时间参数,后续的计算只需关心标准时间相对于0、1的取值即可,大大减少了复杂度。

接下来,Animation考虑的是时间线效果的实现,例如,调用者也许希望实现一个先加速后减速的移动效果,于是Animation需要对刚刚计算出的标准时间再做进一步加工,如下所示:

mInterpolator是一个实现了Iterpolater接口的对象。Interpolator接口定义了一个getInterpolation()函数,用于根据一个现有的标准时间计算其对应的插值时间(或者说应用了时间线效果后的时间)。其子类众多,分别实现了各种各样的时间线效果。调用者可以通过Animation类的setInterpolator()函数设置所期望的效果。以AccelerateDecelerateInterpolator这个插值类的实现来说明一下插值器是如何工作的。其getInterpolation()函数的实现为:

它的返回值实际上是一个余弦函数的图像。如图4-11所示。当标准时间正常地匀速流逝时,插值时间则经历了一个先加速后减速的流逝过程。后续的变换计算直接使用插值时间即可“免费”地获得这个平滑、圆润的效果。

最后,Animation类使用插值时间调用由子类实现的applyTransformation()函数,计算当前的变换:

计算结果将被保存至调用者所提供的Transformation对象中。图4-11 插值时间与标准时间

以最简单的AlphaAnimation为例,applyTransformation()的实现如下所示:

非常简单!这完全得益于Animation类优秀的设计。至此,Animation类的工作原理已经非常清楚了。既然Animation类仅仅根据时间戳计算变换,那么是谁对动画一帧一帧地进行渲染呢?对,就是本章开始时的例子SampleWindow中所使用的Choreographer类。

2.动画驱动器Choreographer类

Choreographer类是Android 4.2新增的一个API。此类的功能与读者所熟知的Handler的post()函数非常类似。区别就在于,Choreographer类处理回调的时机为屏幕的垂直同步(VSync)事件到来之时,其处理回调的过程被当作渲染下一帧的工作的一部分。

Choreographer为驱动动画提供了以下函数: postCallback(int callbackType,Runnable action,Object token)

在下一次VSync时执行action参数所指定的操作。callbackType的取值为CALLBACK_INPUT、CALLBACK_ANIMATION和CALLBACK_TRAVERSAL,表示action所指定的回调的工作内容分别为处理输入事件、处理动画、进行布局。当一次VSync事件到来时, Choreographer将优先执行所有INPUT类型的回调,然后执行ANIMATION,最后才执行TRAVERSAL。这不正是游戏的主循环的工作流程吗?token参数对回调的执行不会产生影响。只是当取消一次回调时,除提供回调对象之外,必须提供相同的Token。 postCallbackDelayed(int callbackType,Runnable action,Object

token,long delayMillis)

同上一个函数一样,不过增加了一个延迟参数。其作用与Handler的postDelayed()函数一致。 postFrameCallback(FrameCallback callback)

在下一次VSync时执行callback所指定的回调。与postCallpack()的本质功能没有太大区别。不过其回调类型被强制为CALLBACK_ANIMATION,而且FrameCallback接口的定义的函数为:doFrame(long frameTimeNanos),参数是一个纳秒级的时间戳。因此这个函数天生就是为处理动画帧所设计的。 postFrameCallback(FrameCallback callback,int timeDelayed)

同上,只是增加了一个延迟时间。

3.使用Choreographer和Animation实现动画

在学习Choreographer和Animation的使用方法后,就可以通过它们勾勒出Android动画的工作骨架了。下面是一个演示类,这个类向外界提供一个名为startAnimation()的函数,用于立刻开始运行参数所指定的动画。

可以看出,在Android 4.2中实现动画的方法为:

通过Cheoregrapher发送一个Runnable以处理一帧动画。在处理动画时,使用Animation.getTransformation()函数获取动画对象所需要进行的变换,然后根据变换对动画对象进行渲染。所谓渲染,可以是绘制对象或者改变对象的属性。如果动画需要继续,则继续向Cheoregrapher发送下一帧的处理请求。注意虽然Cheoregrapher用来处理动画,但是其postCallback()以及postFrameCallback()函数并不会自动重复调用传入的Runnalbe或FrameCallback对象,而是仅仅调用一次。因此,当调用者处理完一帧后,如果还需要处理下一帧,必须重新调用上述函数,否则动画将会停止。

上述的代码是Android 4.2中最基本的动画实现原理,所有动画都是在这个骨架之上实现的,包括复杂的WMS。4.5.2 WMS的动画系统框架

学习Android动画的实现原理之后将以此为基础,研究WMS的动画子系统的框架。

在WMS中,有一个同上一节SampleAnimation.scheduleNextFrame()功能一样的函数,用于启动窗口动画。其定义如下:

负责处理动画帧的mAnimationRunnable的定义为:

也就是说,WMS动画的渲染集中实现在WindowAnimator类的animateLocked()函数中。在深入讨论这个函数之前,先了解一下WindowAnimator以及其组成部分。4.5.3 WindowAnimator分析

1.WindowAnimator的组成

本章4.2节曾经讨论了WMS的窗口管理结构是由以下三种成员组成的:DisplayContent、WindowToken和WindowState,分别对应屏幕、显示组件和窗口本身。其实动画系统中也有与之对应的组成结构。它们分别是以屏幕为动画目标的DisplayContentAnimator、以Activity为动画目标的AppWindowAnimator,以及以窗口为动画目标的WindowStateAnimator。而WindowAnimator则是协调它们工作的管理者。另外,WMS管理的不仅仅有窗口动画,还有一些特效动画,如Dimming、屏幕旋转等。所以与之对应的还有DimAnimator、ScreenRotateAnimation这两种Animator。

这些Animator的作用如下: DisplayContentAnimator:Android 4.2新增的Animator,仅仅用于

存储位于其屏幕上的其他类型的Animator。例如

ScreenRotateAnimation、DimAnimator以及Window-

StateAnimator。 AppWindowAnimator:用于对一个AppWindowToken进行动画处

理。由AppWindowAnimator计算得出的Tranformation将被应用在

此Token所拥有的所有窗口上。AppWindowToken的动画主要是

用来表现Activity的进入与退出。 WindowStateAnimator:用于对一个窗口进行动画处理。它计算

得出的Trans-formation将与AppWindowAnimator、

ScreenRotateAnimation以及父窗口的Window-StateAnimator三者

的Transformation一并应用到窗口的Surface上。所以窗口的动画

其实就是Surface的动画。 ScreenRotateAnimation:用于处理转屏动画。由它所计算出的

Transformation将被应用在其所属屏幕的所有窗口之上。 DimAnimator:用于实现Dimming效果。这个Animator的动画对象

不是窗口,而是一块黑色的Surface。当需要Dimming效果时,

DimAnimator会动画地将这块Surface以一定的透明度衬于需要

Dimming效果的窗口之下。之所以使用一个Animator来完成这个

工作,是为了提供舒服的淡入淡出效果。

除了DimAnimator和DisplayContentsAnimator之外,其他的Animator都有一个名为stepAnimationLocked(int timestamp)函数。这个函数顾名思义,将其状态迁移到由时间戳timestamp所指定的一帧上。完成stepAnimationLocked()的调用之后,Animator便更新了绘制当前帧时其动画目标所需的Transformation。

这些Animator的从属关系如图4-12所示。图4-12Animator的从属关系

2.WindowAnimator的animateLocked()分析

接下来,看一下WindowAnimator是如何处理一帧动画并协调上述5种Animator的工作的。从WindowAnimator.animateLocked()函数开始:

这个函数相对较长,不过结构非常清晰: 首先通过updateAppWindowsLocked()计算AppWindowToken动画

在当前时间所要求的变换。 针对每一个DisplayContentAnimator,计算其屏幕旋转动画在当

前时间所要求的变换。 针对每一个DisplayContent下的每一个窗口,计算其自身动画在

当前时间所要求的变换。 针对每一个DisplayContent下的每一个窗口,将上述3个变换同时

应用到窗口的Surface上,实现窗口动画帧的渲染。 渲染效果动画,包括屏幕旋转以及Dimming效果。 如果动画渲染的过程中,需要布局系统有相应的动作,则将请求

放置在mBulkUpdateParams与mPendingLayoutChanges两个成员

中,动画渲染完毕后统一通过updateAnimToLayoutLocked()将动

作请求发送给布局系统。 如果动画仍需进行,通过WMS.scheduleAnimationLocked()安排

下一帧的处理。注意需要注意的是,WindowAnimator的animateLocked()函数并不是只在运行一个动画,而是一批保存在各个Animator中的动画。

可以看出,影响窗口的最终显示位置的Animator是AppWindowAnimator、Screen-RotateAnimationAnimator以及WindowStateAnimator。限于篇幅,本节将仅探讨三个Animator中最重要的WindowStateAnimator的工作原理。剩下的两个Animator读者可以参照WindowStateAnimator自行研究,因为它们的本质是一致的,而且也比WindowStateAnimator简单。4.5.4 深入理解窗口动画

1.WindowStateAnimator初探

研究窗口动画离不开WindowStateAnimator(以下简称WSA)。WSA在WindowState的构造函数中随之一起被创建,二者互持双方的引用。WindowState保存了窗口管理方面的属性,例如窗口位置、尺寸、主序、子序、父窗口的引用等,而WSA则保存了窗口的Surface的属性。前者侧重于窗口管理,后者则侧重于窗口显示。WSA的几个重要成员变量如下: mSurface保存了窗口的Surface,同时也是WSA的动画目标。 mWin保存了WSA所对应的窗口。 mAnimation是由WMS设置给WSA的一个Animation类的对象,保

存了WMS要求窗口执行的动画。当窗口没有运行动画时,

mAnimation对象保持为空。 mAnimDw和mAnimDh描述了窗口动画时的“容器尺寸”。容器

尺寸一般是DisplayInfo.appWidth/Height,也就是屏幕上排除状态

栏/导航栏等系统窗口之后供应用程序显示的区域尺寸。在为

Animation的属性设置带有“p”后缀时,水平方向与垂直方向的

参考值就是这两个变量。另外,读者可以回顾一下在布局后的处

理过程中使用动画移动窗口位置的情况,当时将这两个值分别设

置成窗口位置变化量,而动画的移动距离都是100%p。 mAnimLayer,对除Wallpaper与IME之外的窗口来说,其值等于

WindowState的mLayer,也就是窗口最终显示次序。在

prepareSurfaceLocked()中,它将作为Surface的layer被设置到显

示系统。 mAlpha和mShownAlpha都表示窗口的Surface的透明度。它们两

个的区别是, mAlpha是由窗口的LayoutParams.alpha指定的,

也就是客户端要求的透明度。而mShownAlpha是mAlpha在混合

了WindowStateAnimator、AppWindowAnimator、

ScreenRotateAnimation以及父窗口的WindowStateAnimator的

Transformation中的alpha分量后的透明度,也就是实际透明度。 mDsDx、mDtDx、mDsDy和mDtDy这4个变量自己本身没有什么

意义,它们组合起来就是Surface的变换矩阵。它们是4个

Animator的Transformation变换矩阵组合之后(相乘)的缩放与

旋转分量。通过Surface.setMatrix()方法可以改变Surface最终显

示的角度与缩放尺寸。 mDrawState保存了窗口的绘制状态。从窗口最初的创建,到最

终得以显示到屏幕上,共经历了NO_SURFACE、

DRAW_PENDING、

COMMIT_DRAW_PENDING,READ_TO_SHOW和HAS_DRAWN5

个状态。后面将深入探讨这5个状态的迁移过程。说明二维变换矩阵是3×3的矩阵,其左上角的2×2子阵负责缩放与旋转。

接下来将从开始动画及渲染一帧动画两个方面学习WSA的工作原理。

2.开始窗口动画(1)动画的选择与设置

在WMS的relayoutWindow()函数中,当窗口由不可见变为可见时,执行了下面一条语句:

WSA.applyEnterAnimationLocked()将会为当前窗口开始一个淡入动画,将窗口显示出来。它调用了WSA中更为通用的启动窗口动画的函数applyAnimationLocked()。

可以看出,WSA并没有给WMS非常大的自由去选择任意动画。WMS需要通过一个transit参数向WSA提出动画的意图,WMP和WSA再根据此意图选择一个既有动画保存在mAnimation中。注意判断一个窗口是否正在运行动画的方法是判断其对应的WSA的mAnimation成员是否为null。

仅仅如此尚不能让窗口动起来,因为WindowAnimator可能正处于空闲状态。即便WindowAnimator正在不停地工作,此WSA并没有位于DisplayContentsAnimator的mWindowAnimators列表中,因此其动画一样不能得到渲染。那怎么办呢?(2)从布局系统到动画系统

重新回到WMS的relayoutWindow()函数,随后它调用了performLayoutAndPlaceSur-facesLocked()函数,发起了一次重新布局。在布局的最终阶段,布局系统调用updateLayoutToAnimationLocked()将包括WSA在内的所有Animator传递给了动画系统。

在完成布局后,布局系统会将所有布局后的窗口的WSA保存至mLayoutToAnim对象中,然后安排动画系统开始处理下一帧动画的渲染。mLayoutToAnim就像一辆列车,载满了WSA驶向动画系统。

再次来到动画系统处理动画帧的入口mAnimationRunnable.run()函数处,第一个调用的函数就是copyLayoutToAnimParamsLocked()。这个函数将位于mLayoutToAnim中的WSA接下车,再保存到DisplayContentsAnimator的mWinAnimators列表中。

经过一番从布局系统到动画系统的辗转,WSA终于出现在DispalyContentsAnimator的mWinAnimators列表中。在接下来的animateLocked()中,WSA将会参与其中。

3.WSA的Transformation计算

回顾一下WindowAnimator.animateLocked(),在完成AppWindowAnimator和Screen-RotationAnimator的Transformation计算后,通过执行performAnimationsLocked()函数计算WSA的Transformation。(1)performAnimationsLocked分析

继续updateWindowsLocked()的代码:

在这段代码中,和我们讨论的内容相关的有以下两点: 通过调用WSA的stepAnimationLocked()更新其所维护的

mTransformation。这个Transformation将在后面

WSA.prepareSurfaceLocked()的调用中影响Surface的最终位置。 如果窗口的绘制状态为READY_TO_SHOW,则通过调用

performShowLocked()函数将绘制状态改为HAS_DRAWN。拥有

HAS_DRAWN状态的窗口在WSA. prepareSurfaceLocked()函数中

被显示出来。先记住这个操作,在后续的小节中将整理窗口的绘

制状态的迁移以及其意义。

接下来看一下WSA的stepAnimationLocked()是如何更新其Transformation的。(2)stepAnimationLocked()分析

stepAnimationLocked()将较多的经历花费在第一帧时动画的初始化与完成最后一帧后的清理工作上。在动画持续的过程中,其工作还是很简单的,就是调用WSA.stepAnimation()函数以更新Transformation,注意没有locked()。

很简单,不是吗?更新Transformation调用Animation.getTransformation()函数即可。其原理在4.1节便已经介绍过。

4.WSA的动画渲染

经过stepAnimationsLocked()之后,WSA的状态已经准备好正式渲染动画帧了。回到WindowAnimator的animateLocked()函数,在完成performAnimationsLocked()之后,再次遍历DisplayContent下的所有WSA,并分别执行它们的prepareSurfaceLocked()。prepare-SurfaceLocked()将完成动画帧的渲染动作。注意prepareSurfaceLocked()是在众多Animator类中WSA所特有的函数。其他Animator的Transformation通过WSA的这个函数影响窗口动画的渲染。(1)mShownFrame、Surface变换矩阵以及mShownAlpha的计算

prepareSurfaceLocked()函数的第一个工作就是通过调用computeShownFrameLocked()计算mShownFrame、Surface的变换矩阵以及mShownAlpha(Surface透明度)的计算。我们可以将其称为渲染参数。

Android提供的动画工具类可以实现平移、旋转、缩放以及透明度4种动画。因此在窗口动画渲染过程中,需要提取这4种分量,并以此设置Surface的相关属性,从而实现动画帧的渲染。其中mShownFrame提取了窗口平移分量,Surface变换矩阵提取了旋转与缩放分量, mShownAlpha自然提取了透明度分量。注意mShownFrame仅仅提取了窗口平移分量,也就是说只有其left和top是变换后的结果,而其宽度与高度则与mFrame保持一致。

上述三个渲染参数的计算过程繁杂却不失规律性:将AppWindowAnimator、Screen-RotationAnimation、父窗口的WSA以及窗口自身的WSA的4个Transformation组合在一起,然后从组合后的矩阵中提取平移分量交给mShownFrame,将旋转与缩放子阵作为Surface的变换矩阵,再将4个Transformation的透明度分量相乘作为mShownAlpha。

下面分析computeShownFrameLocked()的实现。注意这个函数应用了很多的矩阵乘法操作,注意矩阵乘法是不满足交换律的。例如一个平移矩阵T乘以一个缩放矩阵S的结果与S乘以T的结果是不一样的,因此矩阵乘法分为左乘和右乘两种,要格外注意操作顺序。在computeShownFrameLocked()中只使用了右乘。

注意变换的顺序,它们是不能轻易改动的,因为矩阵乘法不满足交换律。为什么Android要采用这个顺序呢?将参与变换的对象按照顺序列出来:动画窗口,动画窗口的父窗口,动画窗口所属的Activity,UniverseBackground,屏幕旋转。不难发现规律,它们是按照从属关系由小到大排列的。这是因为Android希望父窗体的动画应用于所有子对象。例如,当使用上述变换顺序,子窗口的变换矩阵为A而父窗口的变换矩阵为B时,子窗口的最终变换为AB,父窗口的最终变换仍然是B。当子窗口的变换固定,无论父窗口的变换B为任何值,子窗口相对于父窗口的变换固定为A,也就是说父窗口做任何动画,子窗口都会如影随形地相对于父窗口保持静止。因此,倘若读者需要增加一个ActivityGroundAnimator,那么这个变换应插入AppWindowAnimator与UniverseBackground之间。

至此,mShownFrame、Surface的变换矩阵与透明度计算完毕。接下来将它们设置给Surface。(2)设置变换到Surface

看下prepareSurfaceLocked()的实现:

很简单,上文提到的三个渲染参数都被设置到了Surface。用户可以看到,在这一帧中,窗口的位置发生了变化!

5.窗口的绘制状态与从新建到显示的过程

在布局系统与动画系统中曾多次看到窗口的绘制状态这个概念。窗口绘制状态的迁移体现了窗口从最初的创建到显示在屏幕的过程。这一小节将介绍这个状态的一些细节。

第一个状态:NO_SURFACE。窗口的绘制状态保存在WSA.mDrawState中。当一个窗口刚刚被WMS的addWindow()函数创建时,WSA在WindowState的构造函数中被一并创建。此时,窗口的绘制状态为NO_SURFACE,因为在relayoutWindow()之前,窗口是没有Surface的,当然也不可能显示出来。

第二个状态:DRAW_PENDING。随后,客户端调用了relayoutWindow(),此时WMS通过WSA的createSurfaceLocked()为窗口创建了一块Surface。此时窗口的绘制状态被设置为DRAW_PENDING。也就是说,窗口正拥有一块空白的Surface,此时需要客户端在Surface上作画。由于Surface仍是空白状态,因此此时仍不能让窗口显示出来。

第三个状态:COMMIT_DRAW_PENDING。回顾SampleWindow的例子,在通过Canvas完成在Surface上的绘制之后,调用IWindowSession.finishDrawing()函数,通知WMS客户端已经完成在Surface上的绘制。此时,窗口的绘制状态便成为COMMIT_DRAW_PENDING,意思是窗口的绘制已经完成,正在等待由布局系统进行提交,窗口距离显示在屏幕上已经进了一步。

第四个状态:READY_TO_SHOW。之后工作就比较多了,WMS会继续调用performL-ayoutAndPlaceSurfacesLocked()启动一次重新布局。正如在布局后处理中所看到的,会调用WSA的commitFinishDrawingLocked()。如果窗口的状态为COMMIT_DRAW_PENDING时,窗口的状态会再迁移到READ_TO_SHOW,此时窗口距离最终显示已经很接近了。READ_TO_SHOW表示窗口可以随时被显示,但是为什么不直接将其显示出来呢?因为窗口可能属于某一个AppWindowToken,Android希望当AppWindowToken所有的窗口都已准备好后再将它们一并显示出来。当然,如果窗口不属于AppWindowToken,或者AppWindowToken下的所有窗口都已准备好显示或已经显示,commitFinishDrawingLocked()会立刻调用performShowLocked(),进行绘制状态的下一步迁移。

第五个状态:HAS_DRAWN。performShowLocked()在布局系统中被commitFinish-DrawingLocked()调用,也可能在动画系统中被WindowAnimator的updateWindowsLocked()在处理动画帧的过程中调用。performShowLocked()会将窗口的绘制状态进一步迁移为最终状态HAS_DRAWN。拥有这个状态的窗口距离显示已经无限接近了。

最后,在WSA的prepareSurfaceLocked()中,处于HAS_DRAWN状态却未被显示的窗口通过showSurfaceRobustlyLocked()完成最终显示。

值得一提的是,当窗口的旋转方向发生变化后,窗口的状态会被重置为DRAW_PENDING,表示窗口必须重新绘制自己的内容。完成之后,再一步一步按照上述状态迁移将新的内容显示出来。4.5.5 交替运行的布局系统与动画系统

动画系统在处理一帧动画时,同时保持着WMS.mWindowMap以及WMS.mAnimator两个锁。而布局系统则保持着WMS.mWindowMap锁,因此,布局系统的布局过程与动画帧处理过程是互斥的。然而,动画帧处理和布局过程可能是交替的。

1.从布局系统到动画系统

布局的最终阶段,也就是performLayoutAndPlaceSurfacesLocked()的最后位置,会执行一个名为updateLayoutToAnimLocked()函数。这点在“开始窗口动画”中已经介绍过。此函数会刷新动画系统需要在下一帧执行的Animator列表。因此在上一帧还在运行的Animator,经过一次布局后,下一帧可能就忽然不见了,同时,新的Animator有可能被加入列表中并参与下一帧的绘制过程。这个Animator列表被保存在WMS.mLayoutToAnim中。updateLayoutToAnimLocked()立刻向WindowAnimator发出处理下一帧动画的命令。在开始处理下一帧动画时,WindowAnimator会将Animator列表从mLayoutToAnim取出,并逐个处理。

注意,updateLayoutToAnimLocked()和发送处理下一帧动画的命令是无条件的。也就是说,只要进行了一次重新布局,必然会“惊扰”动画系统。

2.从动画系统到布局系统

在分析WindowAnimator的animateLocked()函数时,曾经说明过mBulkUpdateParams和mPendingLayoutChanges成员变量以及updateAnimToLayoutLocked()函数。

在动画的过程中,某些动画的开始或结束,或者某些操作对布局系统来说有着特别的意义,需要布局系统对此做出反应。此时可以将需要布局系统做出变化的要求或状态存入mPendingLayoutChanges以及mBulkUpdateParams中。mPendingLayoutChanges保存了动画系统要求重新布局DisplayContent时所要做的更改,也就是pendingLayoutChanges,而mBulkUpdateParams主要收集了要求修改保存在mInnerFileds的状态的请求。了解布局系统的工作原理后,读者对这两个概念应该很熟悉了。

在处理完一帧后,上述的两个变量便完成了信息的收集工作。在animateLockd()函数的末尾处会调用updateAnimToLayoutLocked(),这将它们保存在WindowaAnimator.mAnimToLayout中并传递给布局系统。布局系统解析这两个变量收集的请求,并检查这些请求是否需要进行重新布局。如果需要,则调用performLayoutAndPlaceSurfacesLocked()开始布局。

因此,窗口布局与动画帧处理是交替运行的。但是最终一定会以布局系统解析mBulkUpdateParams与mPendingLayoutChanges时发现无须重新布局为交替运行的终点。4.5.6 动画系统总结

这一节以WindowAnimator与WindowStateAnimator为例对WMS的动画系统做了介绍。

WindowAnimator是一个强大的驱动器,在它的控制下,多种类型的Animator有条不紊地完成各自的渲染工作。

在WindowAnimator之下有各种类型的Animator,分别掌管不同类型对象的动画。它们是WindowStateAnimator、AppWindowAnimator、ScreenRotationAnimation、DimAnimator以及一个名不副实的DisplayContentsAnimator。除DimAnimator与DisplayContentAnimator以外,其他类型的Animator都有一个stepAnimationLocked()函数用以计算当前时间下动画对象所需的变换。

虽然受到WindowAnimator的管理,但WindowStateAnimator的重要性却非常重要,因为它不仅要进行动画变换的计算,还要管理窗口的Surface,并且所有其他类型的Animator的变换都要汇集到WindowStateAnimator中完成窗口最终变换的计算。在prepareSurfaceLocked()中,WindowStateAnimator完成所有相关Animator的变换的组合过程,并将变换结果设置到Surface中。

剩余的AppWindowAnimator、ScreenRotationAnimation以及DimAnimator等Animator的工作原理与WindowStateAnimator类似,而且更加简单,读者在经过本节学习后应该可以较快地完成对它们的研究。4.6 本章小结

漫长的WMS之旅就此告一段落。这一章首先讨论了WMS的窗口管理结构,然后详细地分析了WMS的布局和动画两个系统的工作原理。这三部分构成了WMS完成所有工作的基础。类似于屏幕旋转等功能性的内容在本章中并没有探讨,因为这些内容不过是在此基础之上的应用,完成本章的学习之后,扩展到WMS的其他内容也就不难了,所以这部分便留给读者研究吧。

另外,在分析的过程中,本章忽略了和壁纸及输入事件相关内容,它们将在第5章以及第6章进行分析。另外,状态栏的实现在WMS中也有一部分内容值得进一步探讨。因此在后面的几章中,仍会再回到WMS中来,重拾被本章忽略的有价值的内容。第5章 深入理解Android输入系统

本章主要内容: 研究输入事件从设备节点开始到窗口处理函数的流程 介绍原始输入事件的读取与加工的原理 研究事件派发机制 讨论事件在输入系统与窗口之间传递与反馈的过程 介绍焦点窗口的选择、ANR的产生以及以软件方式模拟用户操作

的原理

本章涉及的源代码文件名及位置: SystemServer.java

frameworks\base\services\java\com\android\server\SystemServer.java InputManagerService.java

frameworks\base\services\java\com\android\server\input/InputManagerService.java WindowManagerService.java

frameworks\base\services\java\com\android\server\wm\WindowManagerService.java WindowState.java

frameworks\base\services\java\com\android\server\wm\WindowState.java InputMonitor.java

frameworks\base\services\java\com\android\server\wm\InputMonitor.java InputEventReceiver.java

frameworks\base\core\java\android\view\InputEventReceiver.java com_android_server_input_InputManagerService.cpp

frameworks\base\services\jni\com_android_server_input_InputManagerService.cpp android_view_InputEventReceiver.cpp

frameworks\base\core\jni\android_view_InputEventReceiver.cpp InputManager.cpp

frameworks\base\services\input\InputManager.cpp EventHub.cpp

frameworks\base\services\input\EventHub.cpp EventHub.h

frameworks\base\services\input\EventHub.h InputDispatcher.cpp

frameworks\base\services\input\InputDispatcher.cpp InputDispatcher.h

frameworks\base\services\input\InputDispatcher.h InputTransport.cpp

frameworks\base\libs\androidfw\InputTransport.cpp InputTransport.h

frameworks\base\include\androidfw\InputTransport.h5.1 初识Android输入系统

第4章通过分析WMS详细讨论了Android的窗口管理、布局及动画的工作机制。窗口不仅是内容绘制的载体,同时也是用户输入事件的目标。本章将详细讨论Android输入系统的工作原理,包括输入设备的管理、输入事件的加工方式以及派发流程。因此本章的探讨对象有两个:输入设备和输入事件。

触摸屏与键盘是Android最普遍也是最标准的输入设备。其实Android所支持的输入设备的种类不止这两个,鼠标、游戏手柄均在内建的支持之列。当输入设备可用时,Linux内核会在/dev/input/下创建对应的名为event0~n或其他名称的设备节点。而当输入设备不可用时,则会将对应的节点删除。在用户空间可以通过ioctl的方式从这些设备节点中获取其对应的输入设备的类型、厂商、描述等信息。

当用户操作输入设备时,Linux内核接收到相应的硬件中断,然后将中断加工成原始的输入事件数据并写入其对应的设备节点中,在用户空间可以通过read()函数将事件数据读出。

Android输入系统的工作原理概括来说,就是监控/dev/input/下的所有设备节点,当某个节点有数据可读时,将数据读出并进行一系列的翻译加工,然后在所有的窗口中寻找合适的事件接收者,并派发给它。

以Nexus 4为例,其/dev/input/下有evnet0~5六个输入设备的节点。它们都是什么输入设备呢?用户的一次输入操作会产生什么样的事件数据呢?获取答案的最简单的办法就是用getevent与sendevent工具。5.1.1 getevent与sendevent工具

Android系统提供了getevent与sendevent两个工具供开发者从设备节点中直接读取输入事件或写入输入事件。

getevent监听输入设备节点的内容,当输入事件被写入节点时,getevent会将其读出并打印在屏幕上。由于getevent不会对事件数据做任何加工,因此其输出的内容是由内核提供的最原始的事件。其用法如下:

其中,device_path是可选参数,用以指明需要监听的设备节点路径。如果省略此参数,则监听所有设备节点的事件。

打开模拟器,执行adb shell getevent-t(-t参数表示打印事件的时间戳),并按一下电源键(不要松手),可以得到以下一条输出,输出的部分数值会因机型的不同而有所差异,但格式一致:

松开电源键时,又会产生以下一条输出:

这两条输出便是按下和抬起电源键时由内核生成的原始事件。注意其输出是十六进制的。每条数据有5项信息:产生事件时的时间戳([1262.443489])、产生事件的设备节点(/dev/input/event0)、事件类型(0001)、事件代码(0074)以及事件的值(00000001)。其中时间戳、类型、代码、值便是原始事件的4项基本元素。除时间戳外,其他三项元素的实际意义依照设备类型及厂商的不同而有所区别。在本例中,类型0x01表示此事件为一条按键事件,代码0x74表示电源键的扫描码,值0x01表示按下,0x00则表示抬起。这两条原始数据被输入系统包装成两个KeyEvent对象,作为两个按键事件派发给Framework中感兴趣的模块或应用程序。注意一个原始事件所包含的信息量是比较有限的。而在Android API中所使用的某些输入事件,如触摸屏点击/滑动,其中包含了很多的信息,如XY坐标、触摸点索引等,其实是输入系统整合了多个原始事件后的结果。这个过程将在5.2.4节中详细探讨。

为了对原始事件有一个感性的认识,读者可以在运行getevent的过程中尝试一下其他的输入操作,观察一下每种输入所对应的设备节点及4项元素的取值。

输入设备的节点不仅在用户空间可读,而且是可写的,因此可以将原始事件写入节点中,从而实现模拟用户输入的功能。sendevent工具的作用正是如此。其用法如下:

可以看出,sendevent的输入参数与getevent的输出是对应的,只不过sendevent的参数为十进制。电源键的代码0x74的十进制为116,因此可以通过快速执行如下两个命令实现点击电源键的效果:

执行完这两个命令后,可以看到设备进入休眠或被唤醒,与按下实际电源键的效果一模一样。另外,执行这两个命令的时间间隔便是用户按住电源键所保持的时间,所以如果执行第一个命令后迟迟不执行第二个,则会产生长按电源键的效果—出现关机对话框。很有趣不是吗?输入设备节点在用户空间可读可写的特性为自动化测试提供了一条高效的途径。

现在,读者对输入设备节点以及原始事件有了直观认识,接下来看一下Android输入系统的基本原理。5.1.2 Android输入系统简介

上一节讲述了输入事件的源头是位于/dev/input/下的设备节点,而输入系统的终点是由WMS管理的某个窗口。最初的输入事件为内核生成的原始事件,而最终交付给窗口的则是KeyEvent或MotionEvent对象。因此Android输入系统的主要工作是读取设备节点中的原始事件,将其加工封装,然后派发给一个特定的窗口以及窗口中的控件。这个过程由InputManagerService(以下简称IMS)系统服务为核心的多个参与者共同完成。

输入系统的总体流程和参与者如图5-1所示。图5-1 输入系统的总体流程与参与者

图5-1描述了输入事件的处理流程以及输入系统中最基本的参与者。它们是: Linux内核,接受输入设备的中断,并将原始事件的数据写入设

备节点中。 设备节点,作为内核与IMS的桥梁,它将原始事件的数据暴露给

用户空间,以便IMS可以从中读取事件。 InputManagerService,一个Android系统服务,它分为Java层和

Native层两部分。Java层负责与WMS通信。而Native层则是

InputReader和InputDispatcher两个输入系统关键组件的运行容

器。 EventHub,直接访问所有的设备节点。并且正如其名字所描述

的,它通过一个名为getEvents()的函数将所有输入系统相关的待

处理的底层事件返回给使用者。这些事件包括原始输入事件、设

备节点的增删等。 InputReader,是IMS中的关键组件之一。它运行于一个独立的线

程中,负责管理输入设备的列表与配置,以及进行输入事件的加

工处理。它通过其线程循环不断地通过getEvents()函数从

EventHub中将事件取出并进行处理。对于设备节点的增删事

件,它会更新输入设备列表与配置。对于原始输入事件,

InputReader对其进行翻译、组装、封装为包含更多信息、更具

可读性的输入事件,然后交给InputDispatcher进行派发。 InputReaderPolicy,它为InputReader的事件加工处理提供一些策

略配置,例如键盘布局信息等。 InputDispatcher,是IMS中的另一个关键组件。它也运行于一个

独立的线程中。InputDispatcher中保管了来自WMS的所有窗口的

信息,其收到来自InputReader的输入事件后,会在其保管的窗

口中寻找合适的窗口,并将事件派发给此窗口。 InputDispatcherPolicy,它为InputDispatcher的派发过程提供策略

控制。例如截取某些特定的输入事件用作特殊用途,或者阻止将

某些事件派发给目标窗口。一个典型的例子就是HOME键被

InputDispatcherPolicy截取到PhoneWindowManager中进行处理,

并阻止窗口收到HOME键按下的事件。 WMS,虽说不是输入系统中的一员,但是它却对InputDispatcher

的正常工作起到了至关重要的作用。当新建窗口时,WMS为新

窗口和IMS创建了事件传递所用的通道。另外,WMS还将所有窗

口的信息,包括窗口的可点击区域、焦点窗口等信息,实时地更

新到IMS的InputDispatcher中,使得InputDispatcher可以正确地将

事件派发到指定的窗口。 ViewRootImpl,对某些窗口,如壁纸窗口、SurfaceView的窗口

来说,窗口就是输入事件派发的终点。而对其他的如Activity、对

话框等使用了Android控件系统的窗口来说,输入事件的终点是

控件(View)。ViewRootImpl将窗口所接收的输入事件沿着控件

树将事件派发给感兴趣的控件。

简单来说,内核将原始事件写入设备节点中,InputReader不断地通过EventHub将原始事件取出来并翻译加工成Android输入事件,然后交给InputDispatcher。InputDispatcher根据WMS提供的窗口信息将事件交给合适的窗口。窗口的ViewRootImpl对象再沿着控件树将事件派发给感兴趣的控件。控件对其收到的事件做出响应,更新自己的画面、执行特定的动作。所有这些参与者以IMS为核心,构建了Android庞大而复杂的输入体系。

Linux内核对硬件中断的处理超出了本书的讨论范围,因此本章将以IMS为重点,详细讨论除Linux内核以外的其他参与者的工作原理。5.1.3 IMS的构成

同以往一样,本节通过IMS的启动过程探讨IMS的构成。上一节提到,IMS分为Java层与Native层两个部分,其启动过程是从Java部分的初始化开始,进而完成Native部分的初始化。

1.IMS的诞生

同其他系统服务一样,IMS在SystemServer中的ServerThread线程中启动。

IMS的诞生分为两个阶段: 创建新的IMS对象。 调用IMS对象的start()函数完成启动。(1)IMS的创建

IMS的构造函数如下:

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

可以看出,IMS的构造函数非常简单。看来绝大部分的初始化工作都位于Native层。参考nativeInit()函数的实现。

nativeInit()函数创建了一个类型为NativeInputManager的对象,它是Java层与Native层互相通信的桥梁。

看下这个类的声明可以发现,它实现了InputReaderPolicyInterface与InputDispatcher-PolicyInterface两个接口。这说明上一节曾经介绍过的两个重要的输入系统参与者InputReaderPolicy和InputDispatcherPolicy是由NativeInputManager实现的,然而它仅仅为两个策略提供接口实现而已,并不是策略的实际实现者。NativeInputManager通过JNI回调Java层的IMS,由它完成决策。本节暂不讨论其实现细节,读者只要先记住两个策略参与者的接口实现位于NativeInputManager即可。

接下来看一下NativeInputManager的创建:

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载