剖析虚幻渲染体系(18)- 操作系统(Android)

18.13 Android

Android是一个相对较新的操作系统,设计用于在移动设备上运行。它基于Linux内核,Android仅向Linux内核本身引入了一些新概念,使用了大多数Linux设施(进程、用户ID、虚拟内存、文件系统、调度等),有时方式与最初的意图完全不同。

自推出以来,Android已成为应用最广泛的智能手机操作系统之一。它的普及带动了智能手机的爆炸式增长,移动设备制造商可以免费在其产品中使用它。它也是一个开源平台,使其可针对各种设备进行定制。它不仅在第三方应用生态系统有利的以消费为中心的设备(如平板电脑、电视、游戏系统和媒体播放器)中广受欢迎,而且越来越多地被用作需要图形用户界面(GUI)的专用设备的嵌入式操作系统,如VOIP电话、智能手表、汽车仪表板、医疗设备和家用电器。

大量的Android操作系统是用高级语言(Java编程语言)编写的。内核和大量低级库是用C和C++编写的。然而,大部分系统都是用Java编写的,除了一些小的例外,整个应用程序API也是用Java编写和发布的。用Java编写的Android部分倾向于遵循一种非常面向对象的设计,这正是该语言所鼓励的。

18.13.1 Android和Google

Android是一个不同寻常的操作系统,它将开源代码与封闭源代码的第三方应用程序结合在一起。Android的开源部分被称为Android开源项目(AOSP),是完全开放的,任何人都可以自由使用和修改。

Android的一个重要目标是支持一个丰富的第三方应用程序环境,需要有一个稳定的实现和API来运行应用程序。然而,在一个开放源码的世界里,每个设备制造商都可以随心所欲地定制平台,兼容性问题很快就会出现。需要有某种方法来控制这种冲突。

针对Android的部分解决方案是CDD(兼容性定义文档),它描述了Android必须如何与第三方应用程序兼容,文档本身描述了兼容Android设备的要求。然而,如果没有某种方式来加强这种兼容性,它往往会被忽略,需要一些额外的机制来实现这一点。

Android通过允许在开源平台之上创建额外的专有服务来解决这个问题,提供平台本身无法实现的(通常是基于云的)服务。由于这些服务是专有的,它们可以限制允许哪些设备包含它们,因此需要这些设备的CDD兼容性。

谷歌实现了Android,以支持各种专有云服务,谷歌的一系列服务是典型的例子:Gmail、日历和联系人同步、云到设备消息传递以及许多其他服务,有些对用户可见,有些则不可见。在提供兼容应用程序方面,最重要的服务是Google Play。

Google Play是Google的Android应用程序在线商店。通常,当开发者创建Android应用程序时,他们会使用Google Play发布。由于Google Play(或任何其他应用程序商店)是将应用程序交付给Android设备的渠道,该专有服务负责确保应用程序在其交付的设备上运行。

Google Play使用两种主要机制来确保兼容性。第一个也是最重要的一个要求是,根据CDD,随附的任何设备必须是兼容的Android设备,确保了所有设备的行为基线。此外,Google Play必须了解应用程序所需的设备的任何功能(例如有GPS用于执行地图导航),因此应用程序无法在缺少这些功能的设备上使用。

18.13.2 Android的历史

谷歌在2000年代中期开发了Android,在其开发初期收购了一家初创公司Android。今天存在的Android平台的几乎所有开发都是在谷歌的管理下完成的。

18.13.2.1 早期开发

Android股份有限公司是一家软件公司,成立的目的是开发软件以创造更智能的移动设备。最初是针对相机,但由于智能手机的潜在市场更大,人们的目光很快转向了智能手机。最初的目标是通过在Linux之上构建一个可以广泛使用的开放平台来解决当时为移动设备开发的困难。

在此期间,实现了平台用户界面的原型,以展示其背后的想法。为了支持丰富的应用程序开发环境,平台本身瞄准了三种关键语言:JavaScript、Java和C++。

谷歌于2005年7月收购了Android,提供了必要的资源和云服务支持,以继续将Android作为一个完整的产品进行开发。在此期间,一小部分工程师紧密合作,开始为平台开发核心基础设施,并为更高级别的应用程序开发奠定基础。

2006年初,计划发生了重大变化:该平台将完全专注于Java编程语言,而不是支持多种编程语言,用于其应用程序开发。这是一个艰难的改变,因为最初的多语言方法表面上让每个人都对“世界上最好的”感到满意;对于喜欢其他语言的工程师来说,专注于一种语言就像是倒退了一步。

然而,试图让每个人都快乐,很容易让任何人都快乐。构建三套不同的语言API比专注于一种语言需要付出更多的努力,从而大大降低了每种语言的质量。专注于Java语言的决定对于平台的最终质量和开发团队满足重要截止日期的能力至关重要。

随着开发的进展,Android平台与最终将在其上发布的应用程序密切相关。谷歌已经拥有各种各样的服务,包括Gmail、地图、日历、YouTube,当然还有将在Android上提供的搜索服务。在早期平台上实现这些应用程序所获得的知识被反馈到其设计中。这种应用程序的迭代过程允许在开发早期解决平台中的许多设计缺陷。

大多数早期应用程序开发都是在很少有底层平台可供开发人员使用的情况下完成的。该平台通常在一个进程内运行,通过一个“模拟器”,将所有系统和应用程序作为一个进程在主机上运行。事实上,今天仍然有一些旧实现的残留物,比如应用程序。onTerminate方法仍然存在于SDK(软件开发工具包)中,Android程序员使用它编写应用程序。

2006年6月,两个硬件设备被选定为计划产品的软件开发目标。第一款代号为“Sooner”,基于现有的智能手机,配有QWERTY键盘和屏幕,无需触摸输入,该设备的目标是通过利用现有硬件尽快推出初始产品。第二个目标设备代号为“Dream”,是专为Android设计的,可以完全按照设想运行。它包括一个大(当时)触摸屏、滑出式QWERTY键盘、3G收音机(用于更快的网络浏览)、加速计、GPS和指南针(用于支持谷歌地图)等。

随着软件进度计划越来越清晰,两个硬件进度计划显然没有意义。等到Sooner有可能发布的时候,硬件已经过时了,Sooner的努力正在推出更重要的Dream设备。为了解决这个问题,它决定放弃Sooner作为目标设备(尽管该硬件的开发持续了一段时间,直到新的硬件准备就绪),并完全专注于Dream。

18.13.2.2 Android 1.0

Android平台的首次公开发布是2007年11月发布的预览SDK,包括一个运行完整Android设备系统映像和核心应用程序的硬件设备模拟器、API文档和开发环境。在这一点上,核心设计和实现已经到位,并且在大多数方面与我们将要讨论的现代Android系统架构非常相似。该公告包括在Sooner和Dream硬件上运行的平台的视频演示。

Android的早期开发是在一系列季度演示里程碑下完成的,以推动和展示持续的过程。SDK版本是该平台的第一个更正式的版本。它需要将迄今为止为应用程序开发而拼凑起来的所有部分,清理并记录它们,并为第三方开发人员创建一个内聚的开发环境。

开发现在沿着两条轨道进行:接收关于SDK的反馈以进一步完善和最终确定API,以及完成和稳定交付Dream设备所需的实现。在此期间,SDK进行了多次公开更新,最终于2008年8月发布了0.9版本,其中包含了几乎最后的API。

该平台本身一直在快速发展,2008年春季,重点转向稳定,以便梦想得以实现。此时,Android包含了大量从未作为商业产品发布的代码,从C库的一部分一直到Dalvik解释器(运行应用程序)、系统和应用程序。

Android还包含了一些以前从未有过的新颖设计思想,目前尚不清楚它们将如何实现。所有这些都需要作为一个稳定的产品组合在一起,团队花了几个月的时间,想知道所有这些东西是否真的组合在一起并按预期工作。

最后,在2008年8月,该软件稳定并准备发货。产品进入工厂并开始在设备上闪现。9月,Android 1.0在Dream设备上发布,现在称为T-Mobile G1。

18.13.2.3 后续开发

在Android 1.0发布后,开发继续快速进行。在接下来的5年中,该平台进行了大约15次重大更新,从最初的1.0版本中添加了大量新功能和改进。

最初的兼容性定义文件基本上只允许与T-Mobile G1非常相似的兼容设备使用。在接下来的几年中,兼容设备的范围将大大扩大。这一过程的关键点是:

  • 2009年,Android版本1.5到2.0引入了软键盘,以消除对物理键盘的要求,更广泛的屏幕支持(尺寸和像素密度)适用于较低的QVGA设备,以及新的更大和更高密度的设备,如WVGA Motorola Droid,以及一个新的“系统功能”设施,用于设备报告它们支持哪些硬件功能,以及应用程序指示它们需要哪些硬件功能。后者是Google Play用来确定应用程序与特定设备的兼容性的关键机制。
  • 2011年,Android版本3.0至4.0在平台中引入了10英寸及更大平板电脑的新核心支持。核心平台现在完全支持从小型QVGA手机到智能手机和更大的“平板电脑”、7英寸平板电脑和更大平板电脑,再到10英寸以上的设备屏幕尺寸。
  • 由于该平台为更多样的硬件提供了内置支持,不仅支持更大的屏幕,而且支持带或不带鼠标的非触摸设备,因此出现了更多类型的Android设备。这包括谷歌电视、游戏设备、笔记本电脑、相机等电视设备。

重要的开发工作也进入了一些不那么明显的领域:将谷歌的专有服务与Android开源平台进行更清晰的分离。

对于Android 1.0,已经投入了大量工作来创建一个干净的第三方应用程序API和一个不依赖于专有谷歌代码的开源平台。然而,谷歌专有代码的实现往往还没有清理干净,依赖于平台的内部部分,通常,该平台甚至没有谷歌专有代码所需的设施来与之进行良好的集成。为解决这些问题,很快开展了一系列项目:

  • 2009年,Android 2.0版引入了一种体系结构,第三方可以将自己的同步适配器插入联系人数据库等平台API。Google用于同步各种数据的代码被转移到这个定义良好的SDK API。
  • 2010年,Android 2.2版包含了谷歌专有代码的内部设计和实现工作。这种“伟大的分离”干净地实现了许多核心谷歌服务,从提供基于云的系统软件更新到“云到设备的消息传递”和其他后台服务,这样它们就可以与平台分开交付和更新。
  • 2012年,一个新的Google Play服务应用程序交付给了设备,其中包含Google专有非应用程序服务的更新和新功能。这是2010年分拆工作的成果,允许谷歌全面交付和更新云到设备消息和地图等专有API。

18.13.3 Android设计目标

Android平台在开发过程中出现了许多关键设计目标:

1、为移动设备提供完整的开源平台。Android的开源部分是一个自下而上的操作系统堆栈,包括各种应用程序,可以作为一个完整的产品发布。

2、通过强大而稳定的API,强力支持专有的第三方应用程序。如前所述,维护一个既真正开源又足够稳定的平台,以供专有第三方应用程序使用,是一项挑战。Android使用混合的技术解决方案(指定一个定义良好的SDK以及公共API和内部实现之间的划分)和政策要求(通过CDD)来解决这一问题。

3、允许所有第三方应用程序,包括来自谷歌的应用程序,在公平的竞争环境中竞争。Android开源代码被设计为尽可能中立于构建在其之上的高级系统功能,从访问云服务(如数据同步或云到设备的消息API),到库(如谷歌的映射库)和应用商店等丰富服务。

4、提供一种应用程序安全模型,在该模型中,用户不必深深信任第三方应用程序。操作系统必须保护用户免受应用程序的不当行为,不仅是可能导致其崩溃的有缺陷的应用程序,而且还要保护用户对设备和设备上用户数据的更微妙的滥用。用户越不需要信任应用程序,他们就越有自由尝试和安装应用程序。

5、支持典型的移动用户交互:在许多应用程序中花费很短的时间。移动体验往往涉及与应用程序的简短互动:浏览新收到的电子邮件、接收和发送短信或即时消息、联系联系人拨打电话等;Android的目标通常是200毫秒,以冷启动基本应用程序。

6、为用户管理应用程序流程,简化应用程序的用户体验,以便用户在完成应用程序时不必担心关闭应用程序。移动设备也倾向于在没有交换空间的情况下运行,当当前运行的应用程序集需要比实际可用的RAM更多的RAM时,交换空间允许操作系统更优雅地发生故障。为了满足这两个需求,系统需要采取更积极的态度来管理进程,并决定何时启动和停止进程。

7、鼓励应用程序以丰富和安全的方式进行互操作和协作。在某些方面,移动应用程序是对shell命令的回归:它们不是越来越大的桌面应用程序的单一设计,而是针对特定需求而设计的。为了帮助支持这一点,操作系统应该为这些应用程序提供新类型的设施,以便它们协同工作,创建一个更大的整体。

8、创建一个完整的通用操作系统。移动设备是通用计算的一种新表现形式,比我们的传统桌面操作系统更简单。Android的设计应该足够丰富,可以发展到至少与传统操作系统一样的能力。

18.13.4 Android架构

Android是在标准Linux内核之上构建的,只有几个对内核本身的重要扩展。然而,一旦进入用户空间,它的实现就与传统的Linux发行版大不相同,并且以非常不同的方式使用了许多Linux特性。

与传统的Linux系统一样,Android的第一个用户空间进程是init,它是所有其他进程的根。然而,守护进程Android的init进程启动不同,它更多地关注低级细节(管理文件系统和硬件访问),而不是更高级的用户设施,比如调度cron作业。Android还有一个额外的进程层,运行Dalvik的Java语言环境,负责执行用Java实现的系统的所有部分。

下图展示了Android的基本进程结构。首先是init进程,它产生了许多低级守护进程。其中一个是zygote,它是高级Java语言进程的根。

Android进程层次结构。

Android的init不会以传统方式运行shell,因为典型的Android设备没有用于shell访问的本地控制台。相反,守护进程adbd侦听请求shell访问的远程连接(例如通过USB),根据需要为它们分叉shell进程。

由于大多数Android都是用Java语言编写的,所以zygote守护进程及其启动的进程是系统的核心。总是启动的第一个进程称为系统服务器,它包含所有核心操作系统服务,其中的关键部分是电源管理器、包管理器、窗口管理器和活动管理器。

其他进程将根据需要从zygote中创建。其中一些是作为基本操作系统一部分的“持久”进程,例如电话进程中的电话堆栈,必须始终运行。系统运行时,将根据需要创建和停止其他应用程序进程。

应用程序通过调用操作系统提供的库与操作系统交互,这些库共同构成了Android框架(Android framework)。其中一些库可以在该进程中执行其工作,但许多库需要与其他进程(通常是系统服务器进程中的服务)执行进程间通信。

下图显示了与系统服务交互的Android框架API的典型设计,在本例中为包管理器。包管理器提供了一个框架API,供应用程序在本地进程中调用,这里是PackageManager类。在内部,此类必须获得到系统服务器中相应服务的连接。为了实现这一点,在启动时,系统服务器在服务管理器(由init启动的守护进程)中以定义良好的名称发布每个服务。应用程序进程中的PackageManager使用相同的名称检索从服务管理器到其系统服务的连接。

发布并与系统服务交互。

一旦PackageManager与其系统服务连接,它就可以对其进行调用。大多数对PackageManager的应用程序调用都使用Android的Binder IPC机制实现为进程间通信,在这种情况下,调用系统服务器中的PackageManagerService实现。PackageManagerService的实现仲裁所有客户端应用程序之间的交互,并维护多个应用程序所需的状态。

18.13.5 Linux扩展

大部分情况下,Android包括一个提供标准Linux功能的Linux内核。作为操作系统,Android最有趣的方面是如何使用现有的Linux功能。然而,Android系统也依赖于Linux的一些重要扩展。

18.13.5.1 唤醒锁(Wake Lock)

移动设备上的电源管理与传统计算系统不同,因此Android为Linux添加了一个新功能,称为唤醒锁(也称为挂起阻止程序),用于管理系统如何进入睡眠状态。

在传统的计算系统上,系统可能处于两种电源状态之一:正在运行并准备好用户输入,或者处于深度睡眠状态,在没有外部中断(如按下电源键)的情况下无法继续执行。运行时,可以根据需要打开或关闭辅助硬件,但CPU本身和硬件的核心部分必须保持通电状态,以处理传入的网络流量和其他此类事件。进入低功耗睡眠状态相对来说很少发生:要么是通过用户明确地将系统置于睡眠状态,要么是由于用户不活动的时间间隔较长而进入睡眠状态。要退出此睡眠状态,需要来自外部源的硬件中断,例如按下键盘上的按钮,此时设备将醒来并打开屏幕。

移动设备用户有不同的期望。尽管用户可以以一种看起来像让设备进入睡眠的方式关闭屏幕,但传统的睡眠状态实际上并不理想。当设备的屏幕关闭时,设备仍然需要能够工作:它需要能够接收电话、接收和处理传入聊天消息的数据,以及许多其他事情。

与传统电脑相比,人们对移动设备屏幕的开启和关闭要求也更高。移动交互往往会在一天中出现很多短时间:你收到一条消息,打开设备查看它,也许会发送一句话的回复,你遇到朋友遛狗,打开设备给她拍照。在这种典型的移动使用中,从拉出设备到准备好使用的任何延迟都会对用户体验产生显著的负面影响。

考虑到这些要求,一个解决方案是,当设备的屏幕关闭时,不要让CPU进入睡眠状态,这样它就可以随时重新打开。毕竟,内核确实知道什么时候没有为任何线程安排工作,Linux(以及大多数操作系统)将自动使CPU空闲,在这种情况下使用更少的功率。然而,空闲CPU与真正的睡眠不同。例如:

1、在许多芯片组上,空闲状态比真正的睡眠状态使用的功率要多得多。

2、如果某些工作碰巧可用,即使该工作不重要,空闲的CPU也可以随时唤醒。

3、仅仅让CPU空闲并不意味着你可以关闭真正睡眠中不需要的其他硬件。

Android上的唤醒锁允许系统进入更深层次的睡眠模式,而无需像关闭屏幕这样的明确用户操作。带有唤醒锁的系统的默认状态是设备处于睡眠状态。当设备运行时,为了防止它重新进入睡眠状态,需要保持唤醒锁。

当屏幕打开时,系统始终保持一个唤醒锁,防止设备进入睡眠状态,因此它将保持运行,正如我们预期的那样。然而,当屏幕关闭时,系统本身通常不会保持唤醒锁,因此只有当其他东西保持唤醒锁时,系统才会保持睡眠状态。当没有更多的唤醒锁时,系统进入睡眠状态,并且只有在硬件中断的情况下才能退出睡眠。

一旦系统进入睡眠状态,硬件中断将再次唤醒它,就像在传统操作系统中一样。这种中断的一些来源是基于时间的警报、来自蜂窝无线电的事件(例如来电)、传入的网络流量以及按下某些硬件按钮(例如电源按钮)。这些事件的中断处理程序需要对标准Linux进行一次更改:它们需要获得一个初始唤醒锁,以便在系统处理中断后保持系统运行。

中断处理程序获取的唤醒锁必须保持足够长的时间,以便将控制权从堆栈向上转移到内核中的驱动程序,该驱动程序将继续处理事件。然后,内核驱动程序负责获取自己的唤醒锁,之后可以安全地释放中断唤醒锁,而不会有系统返回睡眠的风险。

如果驱动程序随后要将此事件传递到用户空间,则需要进行类似的握手。驱动必须确保其继续保持唤醒锁,直到将事件传递给等待的用户进程,并确保有机会获得自己的唤醒锁。该流也可以在用户空间中的子系统中继续;只要有东西持有唤醒锁,我们就继续执行所需的处理以响应事件。然而,一旦不再保持唤醒锁,整个系统就会返回睡眠状态,所有处理都会停止。

18.13.5.2 内存不足杀手

Linux包括一个内存不足杀手(Out-Of-Memory Killer),它试图在内存极低时恢复。现代操作系统内存不足的情况是模糊的。使用分页和交换,应用程序本身很少会出现内存不足的故障。然而,内核仍然会遇到这样一种情况,即它在需要时无法找到可用的RAM页面,这不仅是为了新的分配,而且是在交换或分页当前正在使用的某个地址范围时。

在这样一个内存不足的情况下,标准的Linux内存不足杀手是寻找RAM的最后手段,这样内核就可以继续它正在做的任何事情。这是通过给每个进程分配一个“坏”级别来完成的,并简单地杀死被认为是最坏的进程。进程的好坏取决于进程使用的RAM数量、运行时间以及其他因素;目标是杀死希望不是关键的大型进程。

Android给内存不足杀手带来了特别的压力。它没有交换空间,因此在内存不足的情况下更常见:除了从最近使用的存储中删除映射的干净RAM页面之外,没有办法缓解内存压力。即便如此,Android使用标准的Linux配置来过度提交内存,也就是说,允许在RAM中分配地址空间,而不保证有可用的RAM来支持它。过度提交是优化内存使用的一个非常重要的工具,因为它通常用于mmap大型文件(如可执行文件),只需要将该文件中的全部数据的一小部分加载到RAM中。

在这种情况下,现有的Linux内存不足杀手并不能很好地发挥作用,因为它更多的是作为最后的手段,而且很难正确识别要杀死的好进程。事实上,Android在很大程度上依赖于定期运行的内存不足杀手来获取进程并做出正确的选择。

为了解决这个问题,Android向内核引入了自己的内存不足杀手,具有不同的语义和设计目标。Android内存不足杀手运行得更为积极:每当RAM变得“低”时低RAM由一个可调参数标识,该参数指示内核中有多少可用的空闲和缓存RAM是可接受的。当系统低于该限制时,内存不足杀手会运行以从其他地方释放RAM。目标是确保系统永远不会进入糟糕的分页状态,这可能会在前台应用程序争夺RAM时对用户体验产生负面影响,因为由于不断的分页输入和输出,它们的执行速度会变慢。

Android的内存不足杀手并没有试图猜测应该杀死哪些进程,而是非常严格地依赖用户空间提供给它的信息。传统的Linux内存不足杀手有一个逐进程的oom_adj参数,可以用来通过修改进程的总体不良分数来引导它走向最佳的进程。Android的内存不足杀手使用相同的参数,但作为一个严格的顺序:具有较高oom_adj的进程总是在具有较低的进程之前被杀死。

18.13.6 Dalvik

Dalvik在Android上实现Java语言环境,负责运行应用程序及其大部分系统代码。从包管理器到窗口管理器,再到活动管理器,几乎所有系统服务进程都是用Dalvik执行的Java语言代码实现的。

然而,Android并不是传统意义上的Java语言平台。Android应用程序中的Java代码是以Dalvik的字节码格式提供的,基于注册机,而不是Java传统的基于堆栈的字节码。Dalvik的字节码格式允许更快的解释,同时仍然支持JIT(Just-in-Time,实时)编译。通过使用字符串池和其他技术,Dalvik字节码在磁盘和RAM中的空间效率也更高。

在编写Android应用程序时,源代码是用Java编写的,然后使用传统Java工具编译成标准Java字节码。然后,Android引入了一个新步骤:将Java字节码转换为Dalvik更紧凑的字节码表示。它是应用程序的Dalvik字节码版本,打包为最终的应用程序二进制文件,并最终安装在设备上。

Android的系统架构在很大程度上依赖于Linux的系统原语,包括内存管理、安全性和跨安全边界的通信。它不使用Java语言作为核心操作系统概念,几乎没有试图抽象出底层Linux操作系统的这些重要方面。

特别值得注意的是Android对进程的使用。Android的设计不依赖Java语言实现应用程序和系统之间的隔离,而是采用传统的操作系统进程隔离方法。这意味着每个应用程序都在其自己的Linux进程中运行,并有自己的Dalvik环境,系统服务器和其他用Java编写的平台核心部分也是如此。

使用进程进行这种隔离允许Android利用Linux的所有功能来管理进程,从内存隔离到进程离开时清理与进程相关的所有资源。除了进程之外,Android完全依赖于Linux的安全特性,而不是使用Java的SecurityManager架构。

Linux进程和安全性的使用大大简化了Dalvik环境,因为它不再负责系统稳定性和健壮性的这些关键方面。不巧的是,它还允许应用程序在实现中自由使用本机代码,这对于通常使用基于C++的引擎构建的游戏尤为重要。

像这样混合进程和Java语言确实会带来一些挑战。即使是在现代移动硬件上,创建一个全新的Java语言环境也需要一秒钟的时间。回想一下Android的设计目标之一,即能够以200毫秒的目标快速启动应用程序。要求为这个新应用程序启动一个新的Dalvik进程将远远超出预算。即使不需要初始化新的Java语言环境,在移动硬件上也很难实现200毫秒的启动。

这个问题的解决方案是我们前面简要提到的合子本地守护进程。Zygote负责启动和初始化Dalvik,直到它可以开始运行用Java编写的系统或应用程序代码。所有新的基于Dalvik的进程(系统或应用程序)都是从合子派生出来的,允许它们在已经准备好的环境中开始执行。

Zygote带来的不仅仅是Dalvik,还预加载了系统和应用程序中常用的Android框架的许多部分,以及加载资源和其他经常需要的东西。

请注意,从Zygote创建新进程需要一个Linux fork,但没有exec调用。新的进程是原始Zygote进程的复制品,其所有的预初始化状态都已设置好并准备就绪。下图说明了一个新的Java应用程序进程如何与原始的Zygote进程相关。在fork之后,新进程有自己独立的Dalvik环境,尽管它通过写页面上的拷贝与Zygote共享所有预加载和初始化的数据。现在剩下的就是让新运行的进程准备就绪,给它正确的标识(UID等),完成需要启动线程的Dalvik初始化,并加载要运行的应用程序或系统代码。

除了发射速度,Zygote还带来了另一个好处。因为只有一个fork用于从中创建进程,所以初始化Dalvik和预加载类和资源所需的大量脏RAM页面可以在Zygote及其所有子进程之间共享。这种共享对于Android环境尤其重要,因为在Android环境中无法进行交换;可以从“磁盘”(闪存)按需分页清理页面(如可执行代码)。然而,任何脏页必须在RAM中保持锁定,无法将它们调出到“磁盘”。

18.13.7 Binder IPC

Android的系统设计主要围绕应用程序之间以及系统本身不同部分之间的进程隔离进行。这需要大量的进程间通信来协调不同进程之间的关系,可能需要大量的工作来实现和正确处理。Android的Binder进程间通信机制是一个丰富的通用IPC工具,大多数Android系统都是建立在它之上的。

Binder架构分为三层,如下图所示。堆栈底部是一个内核模块,它实现了实际的跨进程交互,并通过内核的ioctl函数将其公开,ioctl是一个通用内核调用,用于向内核驱动程序和模块发送自定义命令。在内核模块之上是一个基本的面向对象用户空间API,允许应用程序通过IBinder和Binder类创建IPC端点并与之交互。顶部是一个基于接口的编程模型,其中应用程序声明了它们的IPC接口,而不需要担心IPC在底层如何发生的细节。

Binder IPC架构。

18.13.7.1 Binder内核模块

Binder没有使用现有的LinuxIPC工具,例如管道,而是包含一个特殊的内核模块,它实现了自己的IPC机制。BinderIPC模型与传统的Linux机制有很大的不同,因此它不能在用户空间中高效地在它们之上实现。此外,Android不支持大多数用于跨进程交互的System V原语(信号量、共享内存段、消息队列),因为它们不提供从错误或恶意应用程序中清除资源的强大语义。

Binder使用的基本IPC模型是RPC(远程过程调用)。也就是说,发送进程向内核提交完整的IPC操作,该操作在接收进程中执行;发送方可以在接收方执行时阻塞,从而允许从调用返回结果。(发送方可以选择指定它们不应阻塞,继续与接收方并行执行。)因此,绑定IPC是基于消息的,就像System V消息队列一样,而不是基于Linux管道中的流。Binder中的消息被称为事务,在更高级别上可以被视为跨进程的函数调用。

用户空间提交给内核的每个事务都是一个完整的操作:它标识了操作的目标、发送者的身份以及正在传递的完整数据。内核确定接收该事务的适当进程,并将其传递给进程中的等待线程。

下图说明了交易的基本流程。发起进程中的任何线程都可以创建一个标识其目标的事务,并将其提交给内核。内核生成事务的副本,并将发送者的身份添加到其中。它确定哪个进程负责事务的目标,并唤醒进程中的线程以接收它。一旦接收进程执行,它将确定事务的适当目标并交付它。

基本绑定IPC事务。

对于这里的讨论,我们将事务数据在系统中的移动方式简化为两个副本,一个副本到内核,一个到接收进程的地址空间。实际的实现在一个副本中完成。对于每个可以接收事务的进程,内核都会创建一个共享内存区域。在处理事务时,它首先确定将接收该事务的进程,并将数据直接复制到该共享地址空间中。

请注意,上图中的每个进程都有一个“线程池”,是由用户空间创建的一个或多个线程,用于处理传入事务。内核将把每个传入的事务分派给当前正在该进程的线程池中等待工作的线程。然而,从发送进程调用内核并不需要来自线程池,进程中的任何线程都可以自由启动事务,如图中的Ta。

我们已经看到,给内核的事务标识了一个目标对象;然而,内核必须确定接收进程。为了实现这一点,内核跟踪每个进程中的可用对象,并将它们映射到其他进程,如下图所示,在这里看到的对象只是该进程地址空间中的位置。内核只跟踪这些对象地址,没有任何意义;它们可以是C数据结构、C++对象或位于该进程地址空间中的任何其他对象的位置。

对远程进程中对象的引用由整数句柄标识,很像Linux文件描述符。例如,考虑进程2中的Object2a,内核知道它与进程2关联,并且内核在进程1中为它分配了句柄2。因此,进程1可以将事务提交给目标为其句柄2的内核,内核可以从中确定该事务正在发送给进程2,特别是该进程中的Object2b。

绑定跨进程对象映射。

与文件描述符一样,一个进程中句柄的值与另一个进程的值的含义不同。例如,在上图中,我们可以看到,在进程1中,句柄值2表示Object2a;然而,在进程2中,相同的句柄值2标识Object1a。此外,如果内核没有为另一个进程分配句柄,一个进程就不可能访问另一个过程中的对象。同样在上图中,我们可以看到内核知道进程2的Object2b,但没有为进程1分配句柄。因此,进程1没有访问该对象的路径,即使内核为其他进程分配了句柄。

这些句柄到对象的关联首先是如何建立的?与Linux文件描述符不同,用户进程不直接请求句柄。相反,内核根据需要为进程分配句柄。该过程如下图所示。在这里,我们将查看上一图中从进程2到进程1对Object1b的引用是如何产生的。关键是交易如何在系统中流动,从图底部的左到右。下图所示的关键步骤是:

1、进程1创建包含本地地址Object1b的初始事务结构。

2、进程1向内核提交事务。

3、内核查看事务中的数据,找到地址Object1b,并为其创建一个新条目,因为它以前不知道这个地址。

4、内核使用事务的目标Handle 2来确定这是针对进程2中的Object2a的。

5、内核现在重写事务头以适合进程2,将其目标更改为地址Object2a。

6、内核同样重写目标进程的事务数据;这里它发现进程2还不知道Object1b,因此为它创建了一个新的句柄3。

7、重写的事务被传送到进程2以供执行。

8、在接收到事务后,进程发现有一个新的句柄3,并将其添加到其可用句柄表中。

在进程之间传输Binder对象。

如果一个事务中的一个对象已经为接收进程所知,那么这个流程是相似的,除了现在内核只需要重写该事务,以便它包含先前分配的句柄或接收进程的本地对象指针。这意味着多次将同一个对象发送到进程将始终导致相同的标识,而不像Linux文件描述符那样,多次打开同一个文件将每次分配不同的描述符。当这些对象在进程之间移动时,Binder IPC系统保持唯一的对象标识。

Binder架构本质上为Linux引入了一个基于能力的安全模型。每个Binder对象都是一种功能。将对象发送到另一个进程将授予该进程该能力。然后,接收过程可以利用对象提供的任何特征。一个进程可以将一个对象发送到另一个进程,然后从任何进程接收一个对象,并识别接收到的对象是否与它最初发送的对象完全相同。

18.13.7.2 Binder用户空间API

大多数用户空间代码不会直接与Binder内核模块交互。相反,有一个用户空间面向对象的库,它提供了一个更简单的API。这些用户空间API的第一级相当直接地映射到我们迄今为止所讨论的内核概念,以三个类的形式:

1、IBinder是Binder对象的抽象接口。它的关键方法是transaction,它将事务提交给对象。接收事务的实现可以是本地进程或另一进程中的对象,如果它在另一个进程中,将通过前面讨论的绑定器内核模块传递给它。

2、Binder是一个具体的Binder对象。实现Binder子类为您提供了一个可由其他进程调用的类,关键方法是onTransact,它接收发送给它的事务。Binder子类的主要职责是查看它在这里接收的事务数据并执行适当的操作。

3、Parcel是用于读取和写入Binder事务中的数据的容器。它有读取和写入类型化数据整数、字符串和数组的方法,但最重要的是,它可以读取和写入对任何IBinder对象的引用,使用适当的数据结构让内核理解并跨进程传输该引用。

下图描述了这些类是如何协同工作的,这里我们看到Binder1b和Binder2a是具体的Binder子类的实例。为了执行IPC,进程现在创建一个包含所需数据的Parcel,并通过另一个我们尚未看到的类BinderProxy发送它。每当进程中出现新句柄时,都会创建该类,从而提供IBinder的实现,该实现的transaction方法为调用创建适当的事务并将其提交给内核。

因此,我们之前看到的内核事务结构在用户空间API中被分割:目标由BinderProxy表示,其数据保存在Parcel中。正如我们前面所看到的,事务通过内核,在接收过程中出现在用户空间中时,它的目标用于确定适当的接收绑定器对象,而Parcel是根据其数据构建的,并传递给该对象的onTransact方法。这三个类现在使编写IPC代码变得相当容易:

1、来自Binder的子类。

2、实现onTransact来解码和执行传入调用。

3、实现相应的代码以创建可传递给该对象的事务处理方法的Parcel。

这项工作的大部分在最后两个步骤中,是解组(unmarshalling)和编组(marshalling)代码,需要将我们希望使用简单方法调用编程的方式转换为执行IPC所需的操作。

18.13.7.3 Binder接口和AIDL

BinderIPC的最后一部分是最常用的,一个基于高级接口的编程模型。这里我们不再处理绑定对象和地块数据,而是从接口和方法的角度来思考。

该层的主要部分是一个名为AIDL(用于Android接口定义语言)的命令行工具。这个工具是一个接口编译器,它对接口进行抽象描述,并从中生成定义该接口所需的源代码,并实现对其进行远程调用所需的适当编组和解编组代码。

下面代码显示了AIDL中定义的接口的一个简单示例,这个接口称为IExample,包含一个方法print,接受一个String参数。

package com.example

interface IExample 
{
    void print(Str ing msg);
}

上述代码中的接口描述由AIDL编译,生成下图中所示的三个Java语言类:

1、IExample提供了Java语言接口定义。

2、IExample.Stub是此接口实现的基类。它继承自Binder,这意味着它可以是IPC调用的接收者;它继承了IExample,因为这是正在实现的接口。此类的目的是执行解组:将传入的onTransact调用转换为IExample的适当方法调用。然后它的子类只负责实现IExample方法。

3、IExample.Proxy是IPC调用的另一端,负责执行调用的编组。它是IExample的一个具体实现,实现它的每个方法,将调用转换为适当的Parcel内容,并通过与之通信的IBinder上的事务调用将其发送出去。

基于AIDL的绑定IPC的完整路径如下图所示:

18.13.8 Android应用程序

Android提供的应用程序模型与Linux shell中的正常命令行环境,甚至是从图形用户界面启动的应用程序非常不同。应用程序不是具有主入口点的可执行文件,它是组成该应用程序的所有东西的容器:它的代码、图形资源、关于它对系统是什么的声明以及其他数据。

按照惯例,Android应用程序是一个扩展名为apk的文件,适用于Android Package。这个文件实际上是一个普通的zip存档,包含了应用程序的所有内容。apk的重要内容包括:

1、描述应用程序是什么、它做什么以及如何运行它的清单。清单必须提供应用程序的包名称、Java风格的范围字符串(例如com.android.app.calculator),
其唯一地标识它。

2、应用程序所需的资源,包括它向用户显示的字符串、布局和其他描述的XML数据、图形位图等。

3、代码本身,可以是Dalvik字节码以及原生库代码。

4、签名信息,安全地识别作者。

应用程序的关键部分是它的清单(manifest)——显示为一个名为AndroidManifest.xml的预编译XML文件,在apk的zip命名空间的根中。假设电子邮件应用程序的完整清单声明示例如下图所示:它允许您查看和撰写电子邮件,还包括将本地电子邮件存储与服务器同步所需的组件,即使用户当前不在应用程序中。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.email">
  <application>
    <activity android:name="com.example.email.MailMainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category="" android:name="android.intent.categor y.LAUNCHER" />
      </intent-filter>
    </activity>
    <activity android:name="com.example.email.ComposeActivity">
      <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category="" android:name="android.intent.categor y.DEFAULT" />
        <data android:mimeType="*/*" />
      </intent-filter>
    </activity>
    <service="" android:name="com.example.email.SyncSer vice">
    </service>
    <receiver android:name="com.example.email.SyncControlReceiver">
      <intent-filter>
        <action android:name="android.intent.action.DEVICE STORAGE LOW" />
      </intent-filter>
      <intent-filter>
        <action android:name="android.intent.action.DEVICE STORAGE OKAY" />
      </intent-filter>
    </receiver>
    <provider android:name="com.example.email.EmailProvider" android:author="" ities="com.example.email.provider.email">
    </provider>
  </application>
</manifest>

Android应用程序没有一个简单的主入口点,当用户启动它们时就会执行。相反,它们在清单的<application>标签下发布各种入口点,描述应用程序可以做的各种事情。这些入口点表示为四种不同的类型,定义了应用程序可以提供的核心行为类型:活动、接收者、服务和内容提供者。我们展示的示例显示了一些活动和其他组件类型的一个声明,但应用程序可能声明其中的任何一个或多个。

应用程序可以包含的四种不同组件类型中的每一种在系统中具有不同的语义和用途。在所有情况下,android:name属性都提供实现该组件的应用程序代码的Java类名,系统将在需要时对其进行实例化。

包管理器(package manager)是Android的一部分,用于跟踪所有应用程序包。它解析每个应用程序的清单,收集并索引在其中找到的信息。有了这些信息,它就为客户机提供了查询当前安装的应用程序并检索相关信息的工具。它还负责安装应用程序(为应用程序创建存储空间并确保apk的完整性)以及卸载所需的一切(清理与先前安装的应用程序相关的一切)。

应用程序在清单中静态声明它们的入口点,这样它们就不需要在安装时执行向系统注册它们的代码。这种设计使系统在许多方面更加健壮:安装应用程序不需要运行任何应用程序代码,应用程序的顶级功能始终可以通过查看清单来确定,没有必要保留一个单独的数据库来存储可能与应用程序的实际功能不同步(例如跨更新)的应用程序信息,并且它保证在卸载应用程序后不会留下任何有关应用程序的信息。这种去中心化的方法是为了避免Windows的集中注册表导致的许多此类问题。

将应用程序分解为细粒度组件也有助于我们的设计目标,即支持应用程序之间的互操作和协作。应用程序可以发布提供特定功能的自身片段,其他应用程序可以直接或间接使用这些片段。

在包管理器之上是另一个重要的系统服务——活动管理器(activity manager)。虽然包管理器负责维护所有已安装应用程序的静态信息,但活动管理器确定这些应用程序应在何时、何处以及如何运行。尽管有它的名字,它实际上负责运行所有四种类型的应用程序组件,并为每种组件实现适当的行为。

18.13.8.1 活动(Activity)

活动是通过用户界面直接与用户交互的应用程序的一部分。当用户在其设备上启动应用程序时,实际上是应用程序内部的一个活动,该活动已被指定为此类主入口点,应用程序在其活动中实现负责与用户交互的代码。

上面xml代码所示的示例电子邮件清单包含两个活动。第一个是主邮件用户界面,允许用户查看他们的邮件,第二个是用于编写新消息的单独接口,第一个邮件活动被声明为应用程序的主要入口点,即当用户从主屏幕启动它时将启动的活动。

由于第一个活动是主活动,因此它将作为用户可以从主应用程序启动器启动的应用程序显示给用户。如果他们这样做,系统将处于下图所示的状态,左侧的活动管理器在其流程中创建了一个内部ActivityRecord实例,以跟踪活动。这些活动中的一个或多个被组织到称为任务的容器中,这些容器大致对应于用户作为应用程序的体验。此时,活动管理器已启动电子邮件应用程序的进程和MainMailActivity的实例,以显示其主UI,该UI与相应的ActivityRecord关联。此活动处于称为“已恢复”的状态,因为它现在位于用户界面的前台。

如果用户现在要离开电子邮件应用程序(不退出它)并启动相机应用程序拍照,我们将处于下图所示的状态。请注意,我们现在有一个新的相机进程正在运行相机的主要活动,它在活动管理器中有一个关联的ActivityRecord,是恢复的活动。以前的电子邮件活动也发生了一些有趣的事情:它现在停止了,ActivityRecord保存了该活动的保存状态,而不是恢复。

当一个活动不再在前台时,系统会要求它“保存其状态”这涉及到应用程序创建表示用户当前看到的内容的最少数量的状态信息,并将这些信息返回给活动管理器,并存储在系统服务器进程中与该活动关联的ActivityRecord中。活动的保存状态通常很小,例如包含在电子邮件中滚动的位置,但不包含消息本身,应用程序会将其存储在持久存储中的其他位置。

回想一下,尽管Android确实需要分页,它可以从磁盘上的文件(如代码)映射干净的RAM中进行分页,但它并不依赖交换空间,意味着应用程序进程中的所有脏RAM页都必须留在RAM中。将电子邮件的主要活动状态安全地存储在活动管理器中,可以使系统在处理交换提供的内存时恢复一些灵活性。

例如,如果相机应用程序开始需要大量RAM,系统可以简单地摆脱电子邮件进程,如下图所示。ActivityRecord及其宝贵的保存状态仍被活动管理器安全地保存在系统服务器进程中。由于系统服务器进程承载了Android的所有核心系统服务,因此它必须始终保持运行,因此保存在这里的状态将在我们需要的时候一直保持。

我们的示例电子邮件应用程序不仅具有主UI的活动,而且还包括另一个ComposeActivity。应用程序可以声明任意数量的活动,可以帮助组织应用程序的实现,但更重要的是,它可以用于实现跨应用程序交互。例如,这是Android跨应用程序共享系统的基础,这里的ComposeActivity正在参与其中。如果用户在使用相机应用程序时决定要共享她拍摄的照片,我们的电子邮件应用程序的ComposeActivity是它拥有的共享选项之一。如果选择此选项,将启动该活动并提供要共享的图片。

在上图所示的活动状态下执行该股票期权将导致下图所示的新状态。有一些重要事项需要注意:

1、必须重新启动电子邮件应用程序的进程,才能运行其ComposeActivity。

2、但是,旧的MailMainActivity此时不会启动,因为它不需要。这减少了RAM的使用。

3、摄像机的任务现在有两条记录:我们刚刚进入的原始CameraMainActivity和现在显示的新ComposeActivity。对于用户来说,这些仍然是一个有凝聚力的任务:通过电子邮件发送图片是当前与他们交互的相机。

4、新的ComposeActivity位于顶部,因此恢复;先前的CameraMainActivity不再位于顶部,因此其状态已保存。如果其他地方需要RAM,此时我们可以安全地退出其进程。

最后,让我们看看,如果用户在最后一个状态(即撰写电子邮件以共享图片)下离开相机任务并返回到电子邮件应用程序,会发生什么情况。下图显示了系统将处于的新状态。请注意,我们已将电子邮件任务及其主要活动带回前台,使得MailMainActivity成为前台活动,但应用程序进程中当前没有运行它的实例。

为了返回到上一个活动,系统创建一个新实例,将其返回到旧实例提供的先前保存的状态。此将活动从其保存状态恢复的操作必须能够将活动恢复到用户上次离开时的相同视觉状态。为此,应用程序将在其保存状态中查找用户所在的消息,从其持久存储中加载该消息的数据,然后应用任何已保存的滚动位置或其他用户界面状态。

18.13.8.2 服务(Service)

服务有两个不同的身份:

1、它可以是一个独立的长时间运行的后台操作。以这种方式使用服务的常见示例包括:执行背景音乐播放、在用户处于其他应用程序中时保持活动网络连接(例如与IRC服务器)、在后台下载或上传数据等。

2、它可以作为其他应用程序或系统与应用程序进行丰富交互的连接点。应用程序可以使用它为其他应用程序提供安全API,例如执行图像或音频处理、将文本转换为语音等。

前面所示的示例电子邮件清单包含一个用于执行用户邮箱同步的服务。一个常见的实现将安排服务以固定的间隔运行,例如每15分钟运行一次,在该运行时启动服务,并在完成时停止自身。

这是第一种服务风格的典型使用,即长时间运行的后台操作。下图显示了这种情况下系统的状态,这非常简单。活动管理器创建了一个ServiceRecord来跟踪服务,注意到它已经启动,因此在应用程序的进程中创建了它的SyncService实例。在此状态下,服务处于完全活动状态(如果不持有唤醒锁,则禁止整个系统进入睡眠状态),并且可以自由地做它想做的事情。在这种状态下,应用程序的进程可能会离开,例如,如果进程崩溃,但活动管理器将继续维护其ServiceRecord,并可以在需要时决定重新启动服务。

为了了解如何将服务用作与其他应用程序交互的连接点,我们假设我们希望扩展现有的SyncService,使其具有允许其他应用程序控制其同步间隔的API。我们需要为这个API定义一个AIDL接口,如下所示:

package com.example.email
    
interface ISyncControl 
{
    int getSyncInterval();
    void setSyncInterval(int seconds);
}

要使用此功能,另一个进程可以绑定到我们的应用程序服务,从而访问其接口,将在两个应用程序之间创建连接,如下图所示。此过程的步骤如下:

1、客户端应用程序告诉活动管理器它希望绑定到服务。

2、如果服务尚未创建,活动管理器将在服务应用程序的进程中创建它。

3、服务将其接口的IBinder返回给活动管理器,活动管理器现在将该IBinder保存在其ServiceRecord中。

4、现在,活动管理器拥有了服务IBinder,可以将其发送回原始客户端应用程序。

5、现在具有服务的IBinder的客户端应用程序可以继续在其接口上进行它想要的任何直接调用。

18.13.8.3 接收者(Receiver)

接收者是发生的(通常是外部)事件的接收者,通常在后台和正常用户交互之外。接收器在概念上与应用程序在发生有趣的事情(警报响起、数据连接更改等)时显式注册回调相同,但不要求应用程序运行以接收事件。

上述所示的示例电子邮件清单包含一个接收器,应用程序可以在设备的存储空间变低时发现该接收器,以便停止同步电子邮件(这可能会消耗更多存储空间)。当设备的存储量变低时,系统将发送存储量低的广播代码,以发送给对事件感兴趣的所有接收器。

下图说明了活动管理器如何处理此类广播,以便将其发送给感兴趣的接收者。它首先向包管理器请求对事件感兴趣的所有接收者的列表,该列表被放置在表示该广播的广播记录中。然后,活动管理器将继续遍历列表中的每个条目,让每个相关应用程序的进程创建并执行相应的接收方类。

接收器仅作为一次性操作运行。当一个事件发生时,系统会发现任何对它感兴趣的接收者,并将该事件传递给他们,一旦他们消费了该事件,他们就完成了。没有像我们在其他应用程序组件中看到的那样的ReceiverRecord,因为特定的接收器在单个广播期间只是一个临时实体。每次向接收器组件发送新广播时,都会创建该接收器类的新实例。

18.13.8.4 内容提供者(Content Provider)

我们的最后一个应用程序组件,内容提供者,是应用程序用来相互交换数据的主要机制。与内容提供者的所有交互都是通过使用content:scheme的URI进行的;URI的权限用于找到要与之交互的正确内容提供者实现。

例如,在图10-51的电子邮件应用程序中,内容提供商指定其权限为com.example.email.provider.email。因此,在此内容提供商上运行的URI将从content://com.example.email.provider.email/开始,URI的后缀由提供者自己解释,以确定正在访问其中的哪些数据。在这里的示例中,一个常见的约定是URI:content://com.example.email.provider.email/messages,表示所有电子邮件的列表,而content://com.example.email.provider.email/messages/1提供对键号1处的单个消息的访问。

要与内容提供者交互,应用程序总是要经过一个名为ContentResolver的系统API,其中大多数方法都有一个初始URI参数,指示要操作的数据。最常用的ContentResolver方法之一是query,它对给定的URI执行数据库查询,并返回一个Cursor以检索结构化结果。例如,检索所有可用电子邮件的摘要如下所示:

query("content://com.example.email.provider.email/messages")

尽管这在应用程序中看起来不一样,但当他们使用内容提供者时,实际发生的事情与绑定到服务有许多相似之处。下图说明了系统如何处理我们的查询示例:

1、应用程序调用ContentResolver。查询以启动操作。

2、URI的权限被交给活动管理器,以便它(通过包管理器)找到适当的内容提供者。

3、如果内容提供商尚未运行,则会创建它。

4、一旦创建,内容提供者将其IBinder返回给活动管理器,实现系统的IContentProvider接口。

5、将内容提供者的绑定返回到ContentResolver。

6、内容解析器现在可以通过调用AIDL接口上的适当方法来完成初始查询操作,并返回游标结果。

18.13.8.5 意图(Intent)

在前面所示的应用程序清单中,我们尚未讨论的一个细节是活动和接收方声明中包含的<intent-filter>标记。这是Android的意图功能的一部分,是不同应用程序如何识别彼此以便能够交互和协同工作的基石。

意图是Android用来发现和识别活动、接收者和服务的机制。它在某些方面类似于Linuxshell的搜索路径,shell使用该路径查找多个可能的目录,以便找到与给定的命令名匹配的可执行文件。

意向有两种主要类型:显性和隐性。显式意图是直接标识单个特定应用程序组件的意图;在Linux shell术语中,它相当于为命令提供绝对路径。这种意图的最重要部分是一对命名组件的字符串:目标应用程序的包名和该应用程序中组件的类名。现在回到应用程序前面所示中的活动,该组件的明确意图将是包名为com.example的组件,电子邮件和类名com.example.email.MailMainActivity。

显式意图的包和类名足以唯一标识目标组件,例如上面提及的主要电子邮件活动。从包名称中,包管理器可以返回应用程序所需的所有信息,例如在哪里找到代码。从类名中,我们知道要执行代码的哪一部分。

隐含意图是描述所需组件的特性,而不是组件本身的特性;在Linux shell术语中,这相当于向shell提供一个命令名,shell将其与搜索路径一起用于查找要运行的具体命令。找到与隐含意图匹配的组件的过程称为意图解析。

18.13.9 应用程序沙盒

传统上,在操作系统中,应用程序被视为代表用户作为用户执行的代码。此行为是从命令行继承的,在命令行中,运行ls命令,并期望它作为身份(UID)运行,具有与你在系统上相同的访问权限。同样,当使用图形用户界面启动你想要玩的游戏时,该游戏将有效地作为你的身份运行,可以访问你的文件和许多其他可能不需要的东西。

然而,这并不是我们今天主要使用电脑的方式。我们运行从一些不太受信任的第三方来源获得的应用程序,这些应用程序具有广泛的功能,可以在其环境中执行我们几乎无法控制的各种任务。操作系统支持的应用程序模型与实际使用的应用程序之间存在断开。可以通过一些策略来缓解,例如区分正常用户权限和“管理员”用户权限,并在首次运行应用程序时发出警告,但这些策略并没有真正解决潜在的断开问题。

换言之,传统的操作系统非常擅长保护用户免受其他用户的侵害,但不擅长保护用户不受自身的侵害。所有程序都是靠用户的力量运行的,如果其中任何一个程序行为不当,它都会对用户造成伤害。想想看:在UNIX环境中,你会造成多大的损害?可能会泄露用户可访问的所有信息。你可以执行rm-rf*,为自己提供一个漂亮的、空的主目录。如果这个程序不仅有漏洞,而且是恶意的,它可以加密所有的文件以换取赎金。用“你的力量”运行一切是危险的!

Android试图以一个核心前提来解决这一问题:应用程序实际上是在用户设备上作为来宾运行的应用程序的开发者。因此,应用程序不受任何未经用户明确批准的敏感信息的信任。

在Android的实现中,这一理念相当直接地通过用户ID来表达。安装Android应用程序时,会为其创建一个新的唯一Linux用户ID(或UID),其所有代码都以该“用户”身份运行因此,Linux用户ID为每个应用程序创建一个沙盒,在文件系统中有自己的隔离区域,就像它们为桌面系统上的用户创建沙盒一样。换句话说,Android在Linux中使用了一个现有的功能,但方式新颖。结果是更好的隔离。

18.13.10 进程模型

Linux中的传统进程模型是创建一个新进程的fork,然后是一个exec,用要运行的代码初始化该进程,然后开始执行。shell负责驱动此执行,根据需要fork和执行进程以运行shell命令。当这些命令退出时,Linux将删除该进程。

Android使用的进程略有不同。正如前面关于应用程序的部分所讨论的,活动管理器是Android中负责管理正在运行的应用程序的一部分。它协调新应用程序进程的启动,确定将在其中运行什么,以及何时不再需要它们。

18.13.10.1 期待进程

为了启动新流程,活动经理必须与zygote沟通。当活动管理器第一次启动时,它会创建一个带有合子的专用套接字,当它需要启动一个进程时,通过它发送一个命令。该命令主要描述要创建的沙盒:新进程应作为其运行的UID以及将应用于它的任何其他安全限制。因此,Zygote必须以root身份运行:当它分叉时,它会为运行时的UID进行适当的设置,最后删除root权限并将进程更改为所需的UID。

回想一下在我们之前关于Android应用程序的讨论中,活动管理器维护关于活动执行、服务、广播和内容提供商的动态信息。它使用这些信息来驱动应用程序进程的创建和管理,例如,当应用程序启动程序以新的意图调用系统以启动活动时,活动管理器负责运行新的应用程序。

在新进程中启动活动的流程如下图所示,图中每个步骤的详细信息如下:

1、一些现有进程(如应用程序启动程序)调用活动管理器,目的是描述它想要启动的新活动。

2、活动管理器要求包管理器将意图解析为显式组件。

3、活动管理器确定应用程序的进程尚未运行,然后向合子请求具有适当UID的新进程。

4、Zygote执行一个分叉,创建一个自己的克隆的新进程,删除特权并为应用程序的沙盒适当设置其UID,并在该进程中完成Dalvik的初始化,以便Java运行时完全执行。例如,它必须在分叉后像垃圾收集器一样启动线程。

5、新进程现在是一个完全启动并运行Java环境的zygote克隆,它调用活动管理器,问“我应该做什么?”

6、活动管理器返回有关它正在启动的应用程序的完整信息,例如在哪里找到它的代码。

7、新进程加载正在运行的应用程序的代码。

8、活动管理器向新进程发送任何挂起的操作,在本例中为“启动活动X”。

9、新进程接收启动活动的命令,实例化适当的Java类并执行它。

请注意,当我们开始此活动时,应用程序的进程可能已经在运行。在这种情况下,活动管理器将简单地跳到最后,向进程发送一个新命令,告诉它实例化并运行适当的组件,可能导致在应用程序中运行额外的活动实例(如果合适的话)。

18.13.10.2 进程生命周期

活动管理器还负责确定何时不再需要进程,它跟踪进程中运行的所有活动、接收者、服务和内容提供商,由此可以确定进程的重要性(或不重要)。

回想一下,Android内核中的内存不足杀手使用一个进程的作为一个严格的顺序来确定应该首先杀死哪些进程。活动管理器负责根据进程的状态,通过将其划分为主要使用类别,适当设置每个进程的oom_adj。下表显示了主要类别,最重要的类别排在第一位,最后一列显示了分配给此类进程的典型oom_adj值。

分类描述oom_adj
SYSTEM系统和守护进程进程−16
PERSISTENT始终运行应用程序进程-12
FOREGROUND当前与用户交互0
VISIBLE对用户可见1
PERCEPTIBLE用户知道的东西2
SERVICE运行后台服务3
HOME主页/启动器进程4
CACHED未使用的进程5

现在,当RAM变低时,系统已经配置了进程,以便内存不足杀手首先杀死缓存的进程,以尝试回收足够的所需RAM,然后是主页、服务等等。在一个特定的oom调整级别内,它会先杀死内存占用较大的进程,然后再杀死内存占用较小的进程。

我们现在看到了Android是如何决定何时启动进程的,以及它如何根据重要性对这些进程进行分类的。现在我们需要决定何时退出进程,对吗?或者我们真的需要在这里做更多的事情吗?答案是,我们没有。在Android上,应用程序进程永远不会干净地退出。系统只留下不需要的进程,依靠内核根据需要获取它们。

缓存进程在许多方面取代了Android缺少的交换空间。由于其他地方需要RAM,缓存进程可以从活动RAM中抛出。如果应用程序稍后需要再次运行,则可以创建一个新进程,将其恢复到用户上次离开时所需的任何先前状态。在幕后,操作系统正在根据需要启动、终止和重新启动进程,因此重要的前台操作仍在运行,只要缓存的进程的RAM不会在其他地方得到更好的使用,它们就会被保留。

18.13.10.3 进程依赖

目前,我们对如何管理单个Android进程有一个很好的概述。然而,还有一个更复杂的问题:进程之间的依赖关系。

举个例子,考虑一下我们之前的相机应用程序,它保存着已经拍摄的照片。这些图片不是操作系统的一部分,它们由相机应用中的内容提供商实现。其他应用程序可能希望访问该图片数据,成为相机应用程序的客户端。

进程之间的依赖关系可以发生在内容提供者(通过对提供者的简单访问)和服务(通过绑定到服务)之间。无论哪种情况,操作系统都必须跟踪这些依赖关系并适当地管理进程。

进程依赖性影响两个关键因素:何时创建进程(以及其中创建的组件),以及进程的oom_adj重要性。请记住,进程的重要性是其中最重要的组成部分,它的重要性也是依赖它的最重要进程的重要性。

例如,在相机应用程序的情况下,其进程和内容提供商不正常运行。它将在其他进程需要访问该内容提供商时创建。当访问摄像机的内容提供商时,摄像机进程将被认为至少与使用它的进程一样重要。

为了计算每个进程的最终重要性,系统需要维护这些进程之间的依赖关系图。每个进程都有当前运行的所有服务和内容提供商的列表,每个服务和内容提供商本身都有使用它的每个进程的列表。(这些列表保存在活动管理器内的记录中,因此应用程序不可能对它们撒谎。)遍历进程的依赖关系图涉及遍历其所有内容提供者和服务以及使用它们的进程。

下图说明了一个典型的状态进程,考虑到它们之间的依赖关系。此示例包含两个依赖项,基于使用相机内容提供商向电子邮件添加图片附件。第一个是当前前台电子邮件应用程序,它使用相机应用程序加载附件,将相机进程提升到与电子邮件应用程序相同的重要性。第二种是类似的情况,音乐应用程序使用服务在后台播放音乐,并且在这样做的同时依赖于访问用户音乐媒体的媒体进程。

考虑如果上图的状态发生变化,使得电子邮件应用程序完成了附件加载,并且不再使用相机内容提供商,会发生什么情况。下图说明了进程状态将如何改变。请注意,不再需要相机应用程序,因此它已脱离前景重要性,并降至缓存级别,使相机缓存也使旧地图应用程序在缓存的LRU列表中下降了一步。

这两个示例最终说明了缓存进程的重要性。如果电子邮件应用程序再次需要使用相机提供程序,则该提供程序的进程通常已作为缓存进程保留。再次使用它只需要将进程设置回前台,并重新连接到已经在那里初始化数据库的内容提供商。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值