现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

Android应用程序开发以及背后的设计思想深度剖析(3)

2015-08-07 22:31 工业·编程 ⁄ 共 19654字 ⁄ 字号 暂无评论

1. 支撑应用程序的Android系统

分析一个系统的构成,可以有多个出发点。从不同出发点,我们可从不同侧面分析这个系统的设计,以及为什么要这样设计:

T 从系统结构出发,Android系统给我们的感觉就是一种简洁的,分层式的构架实现,从这种分层式的构架实现角度,我们可以理解这个系统是如何被组织到一起。

T 从系统的运行态角度出发,我们又可以从Android的整个启动过程里做了哪些工作,系统是由哪些运行态的组成部分来构造起来的。

T 从源代码的结构出发,我们可以通过Android的源代码,来分析Android系统具体实现。设计最终都会落实到代码,通过代码,我们可以逆向地看出来这个系统是如何构建起来的。

这种不同的分析角度,通常也包含着不同的用意,一般是在Android开发里从事不同的方向所导向的最方便的一种选择。比如,作为底层移植者而言,大部分都是BSP(Board Support Package)工程师的工作,比较接近硬件底层,一般就会暴力一点,从Linux启动开始分析,得到运行态的Android系统概念。作为高级的软件设计师,看这个系统,可能更多的会是从系统结构出发,来了解这一系统设计上的技巧与原理。而更多的学习者或是爱好者,则会是直接从源代码来着手,一行行代码跟,最后也会理解到Android的系统的构架。

我们前面粗浅地介绍了Android的编程,并非只是浪费篇张,而是作为我们这里独特的分析角度的一种铺垫。我们的角度,由会先看应用程序,然后回过头来再分析Android系统是如何支持应用程序运行的,如何通过精巧设计来完成基于功能来共享的Android系统的。这种角度可能更接近逆向工程,但这时我们也能了解到Android系统构架的各种组织部分,而且更容易理解为什么Android系统,会是这样看上去有点怪的样子。

作为Android这样的系统,或者是任何的智能手持设备上的操作系统,其实最大挑战并非功能有多好、性能有多好,而首先面临是安全性问题。桌面操作系统、服务器操作系统,在设计上的本质都是要尽可能提供一种多人共享化的系统,而所谓的智能手机操作系统,包含了个人的隐私信息,需要在保护这些隐私信息条件下来提供尽可能多的功能,这对安全性要求是一个很大的挑战。特别像Android这样,设计思路本就是用户会随时随地地下载更多应用程序到系统里,相当于是添加了更多功能到系统里,这时就必须解决安全性问题。

以安全性角度出发,Android必然会引入一些特殊的设计,而这些设计带来的一个直接后果就是会带来一些连锁性的设计上的难题:即要保持足够的性能、同时又要控制功耗;即要功能丰富,同时又需要灵活的权限控制;需要尽可能实现简单,但同时又需要提供灵活性。所有这些后续问题的解决,就构成了我们的Android系统。

1.1 Android应用程序运行环境所带来的需求

我们在实现类似于Android应用程序的支撑环境时,必然会遇到一些比较棘手的问题,当我们能够解决这些问题时,我们就得到了我们想要的Android系统。这类问题有:

l 安全性:作为一个功能可灵活拓展的系统,都将面临安全性的挑战。特别是Android又是一个开源操作系统,它不能像iOS那样保持封闭来保持一定的安全性,又不能过于严格地限制开发者自由来达到安全性目的的,而且由于是嵌入式设备,还不能过于复杂,复杂则执行效率不够。于是,安全性是Android这套运行机制最大的挑战。

l 性能:如果Android系统仅仅只是能够提供桌面上的Java Applet,或是老式手机上的JAVA ME那样的性能,则Android系统则会毫无吸引力。而我们的安全性设计,总会带来一定的性能上的开销,这时可能会导致Android系统的表现还不如标准的Java环境。

l 跨进程交互:因为有了Intent与Activity,我们的系统可能随时随地都在交互,而且会是跨进程的交互,我们需要了解Android里独特的进程间通信的方式。

l 功耗控制:所有使用电池供电的设备,都天生地有强烈的功耗控制的需求。随着处理器的能力加强,这时功耗会变得得更大,提供合理的功耗控制是一种天生地需求。

l 功能:如前面所说,我们提供了安全性系统,又不能限制应用程序在使用上的需求,我们应该尽可能多地提供系统里有的硬件资源,系统能够提供的软件层功能。

l 可移植性:作为一个开源的野心勃勃的智能手机操作系统,Android必须在可移植性上,甚至是跨平台上都要表现良好。只有这样,才会有更多厂商乐意生产Android设备,开发者会提供更多应用程序,从而像今天这样形成良性循环的Android生态环境。

我们再来针对这些应用程序所必须解决的问题,一个个地看Android是如何解决的。

1.2 安全性设计

安全性是软件系统永恒的主题,其紧迫程度与功能的可拓展性成正比。越是可以灵活拓展的系统,越是需要一种强大的安全控制机制。世界上最安全的系统,就是一坨废铁,因为永远不可能有新功能加入,于是绝对安全。如果我们可以在其上编写程序,则需要提供一定程度的安全控制,这时程序有好有坏,也有可能出错。如果我们的软件,会通过互联网这样的渠道可以获得,则这种安全上需求会更强烈,因为各种各样的邪恶用意都有可能存在。大体上说,安全性控制会有四种需求:

l 应用程序绝对不能对系统造成破坏。作为一个系统,它的首要目标当然是共享给运行于其上的应用程序以各种系统级的功能。但如果这些应用程序,如果可以通过某种渠道对这个共享的系统造成破坏,这样的系统去运行程序就没有意义,因为这时系统过于脆弱。

l 应用程序之间,最好不能互相干扰。如果我们的应用程序,互相之间可以破坏对方的数据,则也不会有很好的可用性,因为这时单个的应用程序也还是脆弱的。

l 应用程序与系统,应用程序之间,应该提供共享的能力。在安全性机制下,我们也还是需要提供手段,让应用程序与系统层之间、应用程序之间可以交互。

l 还需要权限控制。我们可以通过权限来保护系统,一些非法的代码在没有权限的情况就无法造成破坏。在给不同应用程序提供系统层功能、提供共享时,应用程序有权限才能执行,没有权限则会拒绝应用程序的访问。

解决这类安全性需求的办法,有繁有简,像Android这样追求简洁,当然会使更简洁的方案,同时这套方案得非常可靠。于是Android的运行模型就参考了久经40年考验的单内核操作系统的安全模型。

为了解释得更清楚一点,我们先来从单内核操作系统开始说起。

在计算机开始出现的原始时期,我们的计算机是没有所谓操作系统的。即使是我们的PC,也是由DOS开始,这样的所谓操作系统,实际上也只是把一些常用的库封闭起来,可以支持一个字符的操作界面,运行完一个任务会退回到操作界面,然后才能再运行下一个。这样的操作系统性能不高,在做一些耗时操作则必须等待。

于是,大家又给CPU的中断控制入手,一些固定的中断源(比如时钟中断)会打断CPU操作而强制让CPU进入一段中断处理代码,在这种中断处理代码里加入一些代码跳转执行的逻辑,就允许代码可以有多种执行序列。这样的代码序列被抽象成任务,而这种修改过的中断处理代码则被称为调度器,这样得到的操作系统就叫多任务操作系统,因为这些操作系统上运行的代码像是有多个任务在并行一样。

这种模型实现简单,同时所谓的任务调度只是一次代码跳转,开销也小,实际上我们今天也在广泛地用它,比如大部分的实时操作系统。但在这种模式里有个很致命的缺陷,就是任务间的内存是共享的,这就跟我们想达到的安全性机制不符,应用程序会有可能互相破坏。这是目前大家在实时操作系统做开发的一个通病,90%的比较有历史的实时系统里,大量使用全局变量(因为内存是可以共享访问的),几乎到了无法维护的程度了。大部分情况下,决定代码质量的,并非框架设计,而是写代码的人。当系统允许犯使用全局变量的错误,大家就会隔三差五的因为不小心使用到,而累积到最后,就会是一大坨无法再维护的全局变量。

于是,在改进的操作系统里,不但让每个任务有独立的代码执行序列,同时也给它们虚拟出来独立的内存空间。这时,系统里的每个任务执行的时候,它自己会以为整个世界里只有它存在,于是很开心地执行,想怎么玩就怎么玩,就算把自己玩挂掉,对系统里的其他执行的任务完全没有影响,因为这时每个任务所用的内存都是虚拟出来互相独立的空间。当然,此时还会有一些共享的需求,比如访问硬件,访问一些所有任务都共享的数据,还有可能需要多个任务之间进行通信。这时,就再设立一种特权模式,在这种模式里则使用同一份内存,来完成这种共享的需求。任务要使用这种模式,必须通过一层特殊的系统调用来进入。在Unix系列的操作系统里,我们这里的任务(task)被称为进程,给每个进程分配的独立的内存区域,被称为用户空间,而进程间特权模式下共享的那段空间,被称为内核空间。

特权模式里的代码经过精心设计,确保执行时不出错,这时就完善了我们前面提到的安全性模型。

有了多任务的支持后,特别是历史上计算机曾经极度昂贵,这时大家觉得只一个人占着使用,有了多任务也很浪费,希望可以让多人共享使用。这时,每个可以使用计算机的人,都分配一个标签,可以在操作系统里通过这个系统认可的标签来运行任务。这时因为所执行的任务都互相独立的,加入标签之后,这些任务在执行时也以不同标签为标识,这样就可以根据标签来进行权限的判断,比如用户创建的文件可以允许其他人操作,也可以不允许其他人操作。这种标签通常只是一个整形值,被称为用户ID(User ID,简称uid)。而用户ID在权限上会有一定的共性,可以被组织成群组,比如所有人是一个群组、负责服务器维护的是一个群组等等。于是可以在用户ID基础上进一步得一个群组ID(Group ID,简称gid)的概念,用于有组织的共享。每个世界都需要超人,有了多用户的操作系统,都必须要有一个超人可以求我们于水火,于是在uid/gid体系里,就把uid与gid都为0的用户作为超人,可以完成系统内任何操作,这一用户也一般称为root。

基于调度器与内存空间独立的设计,使我们得到了安全的多任务支持;而基于uid/gid的权限控制,使我们得到了多用户支持。支持多用户与多任务的操作系统,则是Unix系统。我们的Linux内核也属于Unix系统的一种变种。在4、5年前,我们谈及代码时总是会说Unix/Linux系统的什么什么。除了性能上和功能上的细致差异,Linux与其他Unix系统几乎没有区别(当然实现上差异很大)。只不过近年来Linux的表现实在太过威猛,以至于于我们每次不单独把Linux提出来讲,就显得我们没有表现出来滔滔江水般的崇拜之情。

等一下,难道我们这是在普及操作系统的基础知识?

稍安勿躁,我们的这些操作系统知识在Android环境还是很有帮助的。Android系统是借用的Linux内核,于是这个原则性的东西是有效的。而更重要的是,Android的应用程序管理模型,与Unix系统进程模型有极大的相似性,更容易说明问题。

首先,我们的应用程序,不是需要一种安全的执行环境吗?这时,Linux的进程就提供了良好的支持。假如所有的应用程序,都会以非root的uid权限运行(这时就应用程序就不会有权限去破坏系统级功能),又拥有各自独立的进程空间,还可以通过系统调用来进行共享,这时,我们的安全性需求基本上就得到满足了。当然,这时的Android系统,在构架上跟普通的嵌入式Linux方案没有任何区别。

然后,我们再来看uid/gid。在传统的服务器环境里,Linux系统里的uid/gid是一把利器,可以让成千上万的用户登录到上面,共享这台机器上的服务,因为它本就是服务器。即便是我们的PC(个人计算机),我们也有可能使用uid来使多个人可以安全地共享这台机器。但是,放到嵌入式平台,试想一下,咱们的手机,会不会也有多人共享使用?这是不可能的,手机是个私人性的设备。于是,uid在手机上便是废物了。

而传统Linux运行环境里的另外一个问题是共享,作为环境,是提供尽可能友好的环境,让用户可以共享这台机器上的资源。比如文件系统,每个用户共享机器里的一切文件系统,除了唯一个私人目录环境/home之外,所有用户都可以共享一切文件环境。而无论是PC还是服务器,并不完全控制应用的访问环境的,比如应用程序都是想上网就上网。这些在一个更私人化的嵌入式执行环境里则会是灾难性地,想像一下您的手机,您下载的应用程序,想上网就上网,想打电话就打电话,所有的隐私信息,就泄露得一干二净了。

在纯正Linux环境里做嵌入式系统,我们就面临了更精细的权限控制的问题,需要通过权限来限制使用系统里的资源的共享。我们也可以使用很复杂的方案,Linux本身完全是可以提供军用级安全能力的,有selinux这个几乎可以控制一切的方案。但这样的方案实现起来很复杂,而且我们需要考虑到传统Linux环境是需要一个管理员的,如果我们提供一套智能手机方案,同时还要为这台手机配置一个Linux系统管理员,这就太搞笑了。

在Android的设计里,既然面临这样的挑战,于是系统就采取了一种简单有效的方案,这种方案就是基于uid/gid的“沙盒”(Sandbox)。既然uid/gid在嵌入式系统里是个废物,于是我们可以进行废物利用。应用程序在安装到系统里时,则给每个机程都分配一个独立的uid,如果有共享的需求,则给需要共享的应用程序分配同样的gid,应用程序在执行时,我们都使用这种uid/gid来执行应用程序,则可以根据uid/gid来控制应用程序的能力。应用程序被安装到系统后,它就不再使用完整的根目录,只给它一个特定目录作为它的根目录,每个应用程序的根目录,不再是系统的/目录,也是每个应用程序都不一样的。让每个应用程序使用不同根目录环境,技术上倒不复杂,我们Linux上很多交叉编译工具都是这样方式运行的,这样可以让编译环境完全与主机环境隔离,不会因为主机环境里提供的库文件或是头文件而造成编译成功,但无法运行。

同时,当应用程序发生系统级请求时,都会根据uid/gid来决定它有什么权限,有怎么样的权限。

这样的模型就更加符合手机平台的需求了,应用程序都以独立的进程执行,这时应用程序无论写得多糟糕多邪恶,它只能进行自残,而不能破坏其他应用程序的执行环境。而它们的文件系统都是互相隔离的,则这种文件系统上有可能互相破坏的潜在风险也被解决掉了。这时,我们只需要通过一层库文件,或是在内核里加一层机制可以让所有系统功能的使用都需要经过uid/gid进行权限控制就可以了。

这时,造成的另一个问题是,如果是使用这样的沙盒模型,则库文件与资源,每个进程的私有空间里都需要一份。另外,加入权限验证到库文件里,或是到内核里,都不是软件工程上的合理选择:库文件的方式,需要引入一层权限验证到每个应用程序,在通用性上会带来挑战;而修改Linux内核,则是下下策,核心组件是最不应该改动的,有可能影响内核的正常工作,也可能造成Android对内核的依赖,如果哪天我们不想使用Linux内核了怎么办?

假如,我们把Android核心层的功能,抽出来做成一个独立的进程,这些就迎刃而解了。这个(或是可有多个)核心进程,运行在设备真实的根目录(/)环境里,也会有高于用户态的权限。这时,应用程序可以使用一个最小化的根目录,只需要应用程序执行所需要最基本环境,而对系统级的请求,这些应用程序都可以发出请求到核心进程来完成。这时,我们就可以在核心进程里进行权限控制了。

这样,是不是就完美了?首先这种设计没有依赖性,如果我们哪天把Linux内核干掉,换用FreeBSD或是iOS使用的Darwin内核,这种设计也将是有效的(也就是我们有可能在iOS系统里兼容Android应用程序)。而跟我们Unix的进程实现模型作对比的话,是不是有点熟悉?我们可以将应用程序视为进程,核心进程视为内核层,它们之类的通讯方式,则是用户态发生的一层System Call层。是的,这时,我们相当于在用户态环境又抽象出一套操作系统层。

但是,这跟我们前面介绍的Android环境好像是对不上号,我们的Android应用程序不是Java写的吗?这是安全性设计里更高一级的方案,Java是一种基于虚拟机解析运行的环境,也常被称为托管环境(Hosted),因为需要执行的逻辑并不直接与机器打交道,于是更安全。可惜的是,我们的Android系统从2.3之后,就可以使用一种叫Native Activity的逻辑实体,可以使用C++代码来写应用程序(主要用于编写游戏),这会一定程度上影响到这种托管机制带来的安全性。但问题并不是很严重,我们在后面的内容会看到,实际上,Native Activity最终还是需要通过JNI调用,才能访问到Android里的系统功能,因为这部分是由Java来实现的。

我们再来看看,真实的使用Java的Android系统。

从Java诞生之日起,就给予开发者无限的期望,这种语言天生具备的各种特点,曾在一段时间里被认为可以取代其他任何编程语言。

l 跨平台。Java是一种翻译型语言,则具备了“编写一次,到处运行”的能力,天生可以跨平台。

l 纯面向对象。Java语言是一种高级语言,是完全面向对象的语言,不能使用指针、机器指令等底层技术,同时还带自动垃圾回收机制,这样可以使用代码更健壮,编程更容易。

l 重用性。由于纯面向对象,Java语言的重用性很高,绝大部分源代码几乎不需要修改就可以直接使用。由于Java语言的易用性,很多开发的原型系统,都基于Java开发,几乎所有的设计模式都在Java环境里先实现。更因为Java的历史悠久,这种编程资源的积累,使它的重用性优势更加明显。

l 安全的虚拟机。Java是基于虚拟机环境,虚拟机环境实际上是一种通过程序模拟出来的机器执行环境,更安全。所谓执行的代码,只是程序所能理解的一种伪代码,而且代码代码执行的最坏情况,也就仅能破坏虚拟机环境,完全影响不到运行的实际机器。Java虚拟机这样的执行环境,一般被称为托管(Hosted)编程环境,可以进一步将执行代码的潜在破坏能力限制到一个进程范围内,就是像PC上的虚拟机,再怎么威猛的病毒,最多也只是破坏了虚拟机的执行程序,完全影响不到实际的机器,像.Net,Java等都有这样的加强的健壮性。

l 性能。我们并不总是需要再编译执行的,上次翻译出来的代码,我们也可以缓冲起来,下次支持调用机器代码,这样,Java的执行效率跟实际机器代码的效率相关不大。因为虚拟机是软件,我们可以在虚拟机环境里追踪代码的执行历史,在这种基础上,可以更容易进行虚拟机里面代码的执行状况分析,甚至加入自动化优化,甚至可以超过真实机器的执行效率。比如,在Java环境里,我们执行过一次handler.post(msgTouch),当下次通过msgTouch这个Object作为参数来执行handler.post()方法时,我们完全不需要再执行一次,我们已经可以得到其执行的结果,我们只需要把结果操作再做一次。对于一些大运算量而重复性又比较高的代码,我们的执行效率会得到成倍地提升。这种技术是运行态的优化技术,叫JIT(Just In Time)优化,在实际机器上是很难实现的,而几乎所有使用虚拟机环境的编程语言都支持。

所有的Java语言在编程上的优势,都使它可以成为Android的首选编程环境,这跟WebOS选择JavaScript、WindowsPhone选择.Net都是同样的道理。比如安全性,如果我们上面描述的执行模型是纯Java的,则其安全性得到进一步提升。

但是,纯Java环境不要说在嵌入式平台上,就是在PC环境里,也是以缓慢淡定著称的,Java的自优化能力与处理器能力、内存大小成正比。使用纯Java写嵌入式方案,最终达到的结果也就只会是奇慢无比的JAVA ME。另外,Java是使用商业授权的语言,无论是在以前它的创建者Sun公司,还是现在已经收购Sun公司的Oracle,对Java一贯会收取不低的商业授权费用,一旦基于纯粹Java环境来构建系统,最后肯定会造成Android系统不再是免费的午餐。既然Android所需要的环境只是Java语言本身,原始的Java虚拟机的授权又难以免费,这就迫使Android的开发者,开发出来另一套Java虚拟机环境,也就是我们的Dalvik虚拟机。

于是,我们基于多进程模型的系统构架,出于跨平台、安全、编程的简易性等多方面的原因,使我们得到的Android的设计方案成为下面的这个新的样子:核心进程这部分的实现我们还没分析到,但应用程序此时在引入Java环境之后,都变成了通过Dalvik虚拟机所管理起来的更受限的环境,于是更安全。

而在Java环境里,一个Java程序的主入口实际上还是传统的main()入口的方式,而以main()方法作为主入口,则意味着编程时,特别是图形界面编程,需要用户更多地考虑如何实现,如何进行交互。整个Java环境,全都是由一个个的有一定生存周期的对象组合而成,任何一个对象里存在的static属性的main()方法,则可以在Java环境里作为一个程序的主入口得到执行。如果使用标准Java编程,则我们的图形界面编程将复杂得多,比如我们下面的使用Swing编程写出来的,跟我们前面的Helloworld类似的例子:

importjavax.swing.JFrame;

importjavax.swing.JButton;

importjavax.swing.JOptionPane;

importjava.awt.event.ActionListener;

importjava.awt.event.ActionEvent;

publicclass Swing {

publicstaticvoid main(String[] args) {

JFrame frame = newJFrame("Hello Swing");

JButton button = newJButton("Click Me");

button.addActionListener(newActionListener() {

publicvoidactionPerformed(ActionEvent event) {

JOptionPane.showMessageDialog(null,

String.format("<html>Hello from <b>Java</b><br/>" +

"Button %s pressed", event.getActionCommand()));

}

});

frame.getContentPane().add(button);

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

frame.pack();

frame.setVisible(true);

}

}

这种方式里,我们这个Helloworld的例子运行起来,会需要另外有窗口管理器来维护应用程序的状态。通过main()进入之后,会有很大的实现上的自由度,过大的自由会导致最终编写出来的应用程序,在代码实现角度就已经会是千奇百怪的,同时,也加大了开发者在编程上的负担。

而Android的编程上,应该绕开这些Java标准化编程上的局限性,应用程序的行为模式应该被规范化,同时也需要减小应用程序开发时的开销。于是,我们的Android环境里,Activity、Service、Content Provider、Broadcast Receiver,不光有共享功能的作用,实际上还起到了减少编程工作量的作用。大部分情况下,我们的通用行为,已经被基类所实现,编程时只是需要将基类的功能按需求进行拓展。而从完全性的角度,我们也限制了应用程序的自由度,不再是从一个main()方法进入来实现所有的逻辑,而是仅能拓展一些特定的回调点,比如Activity的onStart(),onStop()等。从应用程序的角度来看,实际上编程是这个样子的:

这种实现的技巧,对于Android系统来说,虽然应用程序会通过自己的classes.dex来实现一些各自不同的功能实现,但对于Android系统来说,这些不同实现都是土头土脑的一个样子,可以方便管理。有了这种方便管理之后,实际又能达到灵活的目的,比如我们去修改这种应用程序的管理框架时,应用程序则完全不需要改变,自然而然被管理起来。对于Android的应用程序,则通过这种抽象的工作,大规模地减小了工作量,如果我们很懒不愿意浪费时间写垃圾代码,我们都可以借用Android内部已经提供的实现,而所谓的借用实际上是什么都不用干,由系统来自动完成。而这些具体的生命周期,我们在后面的内容里再讨论它们的意义。

引入了Java对Android来说,带来的好处是明显的,不说可移植性。就是对于安全性设计而言,就已经进一步强化了沙盒式的开发模型。但也还有悬而未决的问题,比如性能,比如说功耗控制。我们先来看Android的性能解决之道。

1.3 跨进程通信

从Android的“沙盒”模型,我们可以知道,这种多进程模型,必须需要一种某种通过在多个进程之间进行消息互通的机制。与其他系统不同的是,Android对于这种多进程之间通信的要求会更高。在Android世界里,出于功能共享的需求,进程间的交互随时都在进行,所以必然需要一种性能很高的解决方案。由于Android是使用Java语言作为运行环境的,这样的情况下,通讯机制就需要跟Java环境结合,可以提供面向对象的访问方式,并且最好能够与自动化垃圾回收整合到一起。

出于这些设计上的考虑,Android最终在选择跨进程通信方式时使用了一种叫Binder的通信机制,绝大部分系统的运行,以及系统与应用程序的交互,都是通过Binder通信机制来完成的。但Android系统里并不只有Binder,实际上,在与已有的解决方案进行整合的过程中,Android也可能会使用其他的跨进程通信机制,比如进程管理上会使用Signal,在处理3G Modem时会使用Socket通信,以及为了使用BlueZ这套比较成熟的蓝牙解决方案,也被迫使用Dbus。

任何一种计算环境,都会有实现的功能部件之间进行交互的需求。如果是统一的地址空间,这时解决方式可以简单粗暴,比如通过读写全局变量等来完成。如果是各部件之间的地址空间互相独立,这就会是多进程的操作系统环境,我们就需要某种在两个进程空间之间传递消息的能力,这就是跨进程通信。一般在多进程操作系统里,这都被称为进程间通信(Inter-Process Communication,IPC)。

在传统的操作系统环境里,IPC进程并非必须的选项,只是一种支持多进程环境设计的一种辅助手段。而由于这种非强制性的原因,IPC机制在长期的操作系统发展历史被约定俗成的固化下来,会包括信号(Signal)、信号量(Semaphore)、管道(PIPE)、共享内存(Share Memory)、消息队列(Message Queue)、套接字(Socket)等。

在这些IPC机制里,对系统唯一的必须选项是信号。要是研究过系统的进程调度方向的,都知道信号不光是进程之间提供简单消息传递的一种机制,同时也被用于进程管理。比如在Linux的进程管理框架里,会通过SIGCHLD(Signal No. 20)的信号来管理父子进程,通过SIGKILL(Signal No. 9)来关闭进程,以及SIGSEGV(Signal No. 11)来触发段错误。所以对于进程管理而言,信号同时也是一种进程管理机制,像SIGKILL,在内核进行进程调度时会立即处理,不进入到用户态,也无法进行被屏蔽。既然构建于Linux内核之上,Android必然会使用到信号。

这些常用IPC机制构造出一种灵活的支持环境,可以让多进程软件的设计者可以灵活地选择。但问题是过于灵活也是毛病,这样的灵活机制也不能对上层交互消息作任何假设,只能作为进程间交互的一种最裸的手段,在上层传输还需要进行封装。每个多进程软件在设计里会自己设计一套内部通讯的方案,在与其他软件有交互时再吵架协商出一套通用的交互方案,最后才能组合出一套整个操作系统级别的通讯机制。比如我们的Linux桌面环境里,Firefox有自己的一套IPC机制、Gnome有自己的一套通过Gconf构建的IPC机制,OpenOffice又有另一套。这些软件刚开始只关注自己的实现和改进时,这种IPC不统一的缺陷还不是那么的严重,到后来需要协同工作时,由于IPC不统一造成的无法共同协作的问题就进一步严重起来,于是又通过一番痛苦的标准化过程,又形成了Linux环境里统一的IPC机制—Dbus。

前车之鉴,后事之师,既然以前的Linux解决方案会因为IPC机制不统一造成了缺陷,于是就不能重蹈复辙了。于是Android权衡了设计上的需求,在性能、面向对象、以进程为单位、可以自动进行垃圾回收的多方位的需求,于是选用了一种叫Binder的通信机制。Binder源自于Palm公司开源出来的一套IPC机制OpenBinder,正如名字上所看到的,Binder比OpenBinder拼写简化了一些,也是OpenBinder的一种简化版。

OpenBinder本用于构建传统的操作系统BeOS的系统级消息传递机制的,在BeOS退出历史舞台之后,又被Palm收购用于Palm的编程环境。但对于Android来说,OpenBinder的设计过于复杂,它本质是非常接近微软的COM通信机制全功能级IPC,在Binder体系里,可用于包装系统里的一切对象,同时也具备像CORBA那样的可以绑定到多种语言支持环境里,甚至它里面还有shell环境支持!这对Android来讲就有点杀鸡用牛刀了。于是Android简化了OpenBinder,只借用其内核驱动部分,上层则重新进行封装,于是得到我们常说的Binder。从学习角度而言,我们只需要理解与分析Binder,也不应该被一般误导性的说明去研究OpenBinder,OpenBinder绝大部分功能是在Android里碰不到的,而Android里的Binder实现与应用的完整源代码,总共也没几行,分析起来更容易一些。

Binder构建于Linux内核里的一个叫binder的驱动之上,系统里所有涉及Binder通信的部分,都通过与/dev/binder的设备驱动交互来得到信息互通的功能。而binder本身只是一种借用内存作后端的“伪驱动”,并不对应到硬件,而只是作用于一段内存区域。通过这个binder驱动,最终在Android系统里得到了进程间通信所需要的特点:

T 高性能:基于Binder的通信全都使用ioctl来进行通信,做过实时系统的人都会知道,ioctl则会绕开文件系统缓冲,达到实时交互的目的。同时,基于Binder传递的数据,binder可以灵活地选用不同的机制传递数据,两次复制(用于传递小数据量),一次复制(用于ServiceManager),零复制(通过IMemory对象共享,数据只在内核空间里存在一份,用户态进行内存映射),于是在效率上灵活性都可以很高。

T 面向对象:与传统的仅提供底层通信能力的IPC不同,Android系统是一种面向对象式开发的系统,于是需要更高层的具备面向对象的抽象能力的IPC机制。使用底层IPC加以封装也不是不可以,像Mozilla这种设计也可以解决问题,但Android是作为操作系统开发的,没必要产生这样的开销。而使用Binder之后,就得到了天然的面向对象的IPC,在设计上与实现上都得到了简化。使用Binder进行通信异常简单,只需要通过直接或是间接的方式继承IBinder基类即可。

T 绑定到Dalvik虚拟机:一般的系统设计里使用IPC,都是先将就底层,再设计更面向高层的IPC接口。还拿Mozilla来作例子的话,Mozilla里的IPC先是提供C/C++编程接口,然后再绑定到其他高级语言,像Java、JavaScript、Python。而Android里的Binder则不同,更多地是面向Java的,直接通过JNI绑定到Java层的Dalvik虚拟机,先满足Java层的编程需求,然后再考虑到C/C++。使用C/C++语言来对Binder进行编程,更像是在Java底层的hack层,无论Binder怎么被扩展,其服务对象都是Java层的,从这个意义上来说,Android的Binder,也是面向Java的。

T 自动垃圾回收:在传统的IPC机制里,垃圾回收或者说内存回收,都是IPC编程框架之外的,IPC本身只负责通信,并不管理内存。而Android里使用Binder也不一样,因为它是面向对象式的,于是更容易使用基于对象引用计数的内存管理。在使用Binder时,我们可能会经常遇到sp<>,wp<>这样的模板类,这种模板则是直接服务于垃圾回收的,以Java语言的Soft Reference、Weak Reference来管理对象的生存周期。

T 简单灵活:与传统IPC相比,或是标准OpenBinder实现相比,Binder都具备了实现更简单灵活的特点。Binder在消息传递机制之上,附加了一定的内存管理功能,大大简化了编程,同时在实现上又非常简单,大家可以看到frameworks/base/libs/binder下的实现,也没有几行代码,而对于驱动来说,也仅有一个drivers/stage/android/binder.c一个文件而已。这种简单实现也给上层使用上提供了更多灵活性。

T 面向进程:除了Signal之外,传统IPC机制几乎没有办法获取访问者(也就是进程)相关的信息,而只以内核态的文件描述符为单位进行通信。但Binder在设计里,就是以进程为单位的,所有的信息在传递过程里都以PID作为分发的基础,这时也为Android的多进程模型提供了便捷性。在维护进程之间通信状态时,Binder底层本身是可以得到进程是否已经出错挂掉等信息。Binder这种特性也间接提供了一定的进程调度的能力,处于Binder通信过程里的进程,在没有Binder通信发生时,实际一直会处于休眠状态,并不会盲目运行。

T 安全:我们前面分析过基于进程空间的“沙盒”模型,它是基于uid/gid为基础的单进程模型。如果我们的Binder提供的传递机制也是以进程为单位进行通信,这时这种进程间通信的模型也可以得以强化,进程运行时的uid/gid也会在Binder通信时被用于权限判断。

当然,Android系统并非依赖Binder处理全部逻辑,在特殊性况下也会使用到其他的IPC机制,比如我们前面提到的RIL与BlueZ。在与进程管理模型相适配时,Android也会使用到信号,关闭应用程序是通过简单的SIGKILL,还会在某些代码调试部分的实现里使用SIGUSR1、SIGUSR2等。但对于Android整个系统的核心实现而言,都会通过Binder来组织系统内部的消息传递与处理。应用程序之间的万能信息Intent,实际上底层是使用的Binder,而应用程序要访问到系统里的功能,则会是使用Binder封装出来的Remote Service。整个系统的基本交互图如下所示:

在binder之上,framework里会把binder通信的基本操作封装到libbinder库(frameworks/base/libs/binder)实现里,当然libbinder则会通过JNI绑定到Dalvik虚拟机的执行环境。应用程序App1与App2之间进行通信,只会通过Binder之上再包装出来的Intent来完成具体的交互。同时,在Android的系统实现的,我们提供的系统级功能,大部分会是Java实现的Remote Service,有一小部分是使用的C/C++实现的Native Service,而所有的这些Service,会通过一个叫ServiceManager的进程来进行管理,不论是Java实现的Service还是NativeService,都将使用addService()注册到ServiceManager里。当任何一个应用程序需要访问系统级功能时,由会通过调用ServiceManager的getService方法取回一个系统级Service的实例,然后再与这些Service进行交互。

图中我们可以看到,实线代表交互,虚线代表基于Binder的远程交互,从上图中我们也可以看出,实际上,系统里基本上都不会有多个功能实现间的直接调用,所有的可执行部分,都只是通过libbinder封装库来与binder设备进行通信,而通信的结果就是实现了虚线所示的跨进程间的调用。当然,在代码里是看出来这些猫腻的,我们代码上可能大部分都貌似是直接调用的,比如WIFI访问:

mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);

intwifiApState = mWifiManager.getWifiApState();

if (isChecked&& ((wifiApState == WifiManager.WIFI_AP_STATE_ENABLING) ||

(wifiApState == WifiManager.WIFI_AP_STATE_ENABLED))) {

mWifiManager.setWifiApEnabled(null, false);

}

我们在代码调用上根本看不到有Binder存在,就像是在当前进程里调用了某个方法一样。在这代码示例里,我们只是通过getSystemService()取回一个Context.WIFI_SERVICE的实例,然后就通过这一实例来访问gitWifiApState()方法,但在底层实现上,getSystemService()与getWifiApState()都是运行另一个进程空间里的代码。这就是Android在实现上的厉害之处,虽然是简单的封装,但使我们的代码具备了强大跨进程的功能。

而这些所谓的Service,跟我们在应用程序编程里看到的Service基本上是一样的,唯一的区别是会通过addService()加载到ServiceManager的管理框架里,于是所有的进程都可以共享这种Service。在addService()成功之后,应用程序或是系统的其他部分,不需要再通过Intent来调用Service,而是可以直接通过getService()取回被调用的实例,然后直接进行跨进程调用。当然,Service概念的引入也给系统设计带来了方便,这些Service,即可以以独立进程的方式运行,也可以以线程方式被托管到另一个进程。在Android世界里,这样的技巧被反复使用,一般情况下,都会将系统级的Service以线程方式运行在SystemServer这个系统进程里,但如果有功能上或是稳定性上的考虑,则有可能以独立的进程来执行Service,比如MediaServer、StageFlinger就都是这样的例子。但对于调用它们的上层来说,都是透明的,无论哪种方式都没有区别。

对于我们应用程序,我们最关心的可能还是Intent。其实,如何回到前面我们提及过的onSavedInstance这种序列化对象,我们就可以了解到Intent这种神奇机制是如何实现的了。我们前面说明了,其实onSavedInstance的类型是Bundle,是一个用于恢复Activity上下文现场的用于序列化处理的特殊对象,而这种所谓序列化,就是将其转换成字典类型的结构,以key对应value的方式进行保存与读取。Bundle类可以处理一些Java原始支持的数据类型,像String,Int等,可以使只使用这些数据类型的对象可以被序列化(字典化),于是可以将这样的Bundle对象保存起来或是在跨进程间传递。同时,Bundle里可以把另一个Bundle对象作为其属性变量,于是通过Bundle实际上可以描述所有对象,而且是以一种进程无关机器无关的状态描述,这种结构天生就可以跨进程。

像我们的这例子里,我们可以通过Bundle来描述Bundle_Master对象,但因为Bundle_Master里又包含另一个对象,于是使用Bundle_Key4来描述。这些在编程上则不是直接使用Bundle对象进行进程间的数据传递,因为需要使用Binder来传递对象,于是我们又会得到一个Parcel类,把字典结构的Bundle对象,转换成基于Binder IPC进制的Parcel对象。Parcel对象,实际上也没有什么特殊性,只是提供一种跨进程交互时的简便性罢了,它内部使用一个Bundle来存储中间结果,同时会通过Binder来标识访问的活跃状态,然后提供writeToParcel()与readFromParcel()两个读写的接口方法也提供中间数据的读写。在使用Parcel进行封装时,可以通过实现Parcelable接口来实现。最后得到的Parcel对象如下所示:

如上所示,实现了一个Parcel接口之后,我们得到的某个Parcelable的类,内部除了自己定义的属性与方法之外,还需要提供一个static final的CREATOR对象。Static final表明,所有这些Parcelable的对象(比如我们例子里的Intent),都会共享同一CREATOR对象来初始化对象,或是对象的数组。此时便提供了对象初始化方法的跨进程性。进一步需要提供两个接口方法,readFromParcel()与writeToParcel()两个方法,则此时对象就可以通过这两个接口来进行不同上下文环境里的读写,也就是对象本身可被“自动”序列化了。当然,对于Bundle内保存的中间数据,有可能也需要单个的读写,所以也会提供readValue()与writeValue()方法来进行内部属性的读写,严格地说是Getter/Setter。我们这里是以Intent为例子说明的,这我们就可以看到Intent的起源,其实Intent也只是通过Binder来拓展出来的Bundle序列化对象而已。

在我们Intent对象里包含的信息里,绝大部分是Java的基本类型,比如Action,Data都是String,于是大部分情况下我们都是直接Parcelable的接口操作即可完成共享。而唯一需要进一步使用Parcel的,是Extras属性,通过Extras可以传递极其复杂的对象为参数,于是Extras便成为Intent的属性里唯一会被Parcel进一步包装的部分。

通过Binder可以很高效而且安全实现数据的传递,于是我们整个Android世界便毫无顾忌地使用Intent在多进程的环境里运行,Intent的发送与处理,实际上一直处理多进程的交互环境里,用户本质上并没有进程内与跨进程的概念。

对于应用程序之间是如此,对于应用程序与系统之间的交互也是如此,我们也需要达到一种简单高效,同时让应用程序看不到本地与远程变化的效果。Android对于Java环境里的系统级编程,也提供了良好的封装,使Java环境里编写系统级服务也很简单。

使用Binder之后,所有的跨进程的访问,都可以通过一个Binder对象为接口,屏蔽掉调用端与实现端的细节:

比如,我们这个应用环境里,进程1访问进程2的ClassOther时,我们并非直接访问另一进程的对象,而在进程1的地址空间里,会有一个objectRemote的远程引用,这一远程引用通过一个IBinder接口会引用到进程2的ClassRemote的实例。当在进程1里访问进程2的某个方法时,则直接会通过这一引用调用其ClassRemote里实现的具体的方法。

这种命名方式跟我们的Android对不上号,这是因为Android里使用了一种叫Proxy的设计模式。Proxy设计模式,可以将设计与实现分离开。先定义接口类,调用端会通过一个Proxy接口来进行调用,而实现端则只通过接口类的定义来向外暴露其实现。

如图所示,调用方会通过接口找到Proxy,而通过Proxy才会具体地找到合适的Stub实现来进行通信。通过这样的抽象,可以使代码的互相合作时的耦合度被大大降低,Proxy实现部分可以根据调用的具体需要来向不同的实现发出调用请求,同时实现接口的部分也会因此而灵活,对同一接口可以有多个实现,也可以对接口之外的调用进行灵活地拓充。

对应到Android环境,我们就需要能够将Binder的实现,进一步通过Proxy设计模式包装起来,从而能够提高实现上的灵活性。于是Android里应用程序与系统的交互模型,就会变成这样:

首先继承自Binder的,不再是某一个对象,而是被拆分成Proxy与Stub两个部分,Proxy部分被重命名为xxxManager(与现实生活相符,我们总是不直接找某个人,而是找他的经验来解决问题),而Stub部分则是通过继承自Binder来得到一个Stub对象,这个Stub对象会作为一个xxxService的属性,从而对于某些功能xxx的存在周期,将通过Service的生命周期来进行管理。通过这样的代码重构的Proxy访问模式,使我们的系统的灵活性得以大大提高,这也是为什么我们在系统里可以见到大量的xxxManager.java,xxxService.java的原因,Manager供应用程序使用,属于Proxy运行在应用程序进程空间,Service提供实现,一般运行在某个系统进程空间里。

通过这种Proxy方式简化之后,可能还是会有个代码重复性的问题,比如我们的Manager怎么写、Service怎么写,总是会不停地重复写这些交互的代码,但对于通信过程而言,本质上从Manager的信息,怎么到Service端,基本上是比较固化的。于是,Android里提供了另一套机制,来简化设计,减小需要重复的代码量。

AIDL跟传统的IDL有类似之处,但区别也很大,更加简单,只用于Java环境,不能用于网络环境里。AIDL提供的功能,就是将这些重复性的通信代码(Proxy与Stub的基本实现),固化到AIDL工具自动生成的代码里,这部分代码用户看不到,也不用去改写它。

这时,在Android里,写一个系统级的Service,就不再有明显的Proxy类、Service类,而变成调用端直接调用,实现端只提供Stub的具体实现即可。当然,我们系统环境里的Proxy接口会复杂一些,需要考虑权限、共享、电源控制等多种需求,所以我们还是可以见到大量的Manager类的实现。

我们在Android系统编程里研究得深入一点,也会发现Remote Service,也就是我们AIDL机制的妙用,很简单地就可以提供一些方法或是属性给另一个进程。Android系统,其实说白了,也就是大量这样的基于AIDL的Remote Service的实现。

当我们遇到性能问题时,我们还可以在Binder之上,“黑”掉正常的Java代码,全部使用C/C++来响应应用程序的请求,这就是所谓的Native Service。当然,我们就不能再使用AIDL了,AIDL是专用于Java环境里跨进程调用所用的,必须自己手动来实现所有的Proxy接口与Stub接口。从这个意义上来说,Android也是Java操作系统,因为使用C/C++反而是比较受限的编程环境。

于是,我们的跨进程通信的问题,几乎可以得到完美解决了。可以再强调一下的是,在Android世界里,如果是应用程序编程,可能只会与Activity打交道,因为大部分情况下,我们只是把界面画出来进行交互。而学习Android底层,需要做Android移植,甚至需要进行Android定制化的改进,对Binder、Remote Service、Native Service的深入理解与灵活使用则是关键点。不了解这些特点的Android系统工程,都将在性能、内存使用上遇到难以避免的麻烦,因为系统本身都依赖于这种机制而存在。由于这样的重要性,我们在后面会专门以这三个主题展开分析,说明在实践运用时,我们可以怎么灵活使用这三种功能部件。

作者:宋宝华

给我留言

留言无头像?