Android服务调用的模式

管理员账号

2018-09-05

Android 的框架服务都是实现在system_server 的各个线程中的。因此应用调用它们时,必须使用进程间通信(IPC,Inter Process Communication)的方式。这就是Binder(Android 特有的IPC 机制)发挥作用的地方。应用需要先在自己这个进程中调用Binder,获取一个端点描述符,然后才能与远程服务建立连接。服务中提供的各种方法是通过IPC 消息进行调用的,这一模式,也被称为远程过程调用(RPC,Remote Procedure Call)。

IPC?RPC?

术语IPC 和RPC 经常会被混用——尽管通常是不正确的。由于这两个概念是讨论Android 服务的基础,所以有必要在这里把二者的区别讲清楚。

进程间通信(IPC,Inter Process Communication):这个概念泛指进程之间任何形式的通信行为,是个可以拿来到处套的术语。它不仅仅包括各种形式的消息传递,还可以指共享资源(最明显的就是共享内存),以及同步对象[mutex 或者其他类似的东西,即确保安全地并发访问共享资源(也就是防止两个或两个以上的对象同时对同一个数据成员进行修改,从而导致的数据被破坏,或者竞争条件下同时读/写数据而导致错误的情况发生)的东西]。

远程过程调用(RPC,Remote Procedure Call):这个术语特指一种隐藏了过程(方法)调用时实际通信细节的IPC 方法。(在使用RPC 时,)客户端将调用一个本地方法,而这个本地方法则是负责透明地与远程服务端(这个远程服务端甚至可以在不同的时间段里是不同的机器)进行过程间通信。这个本地方法会将相关参数顺序打包到一个消息中(这一动作即“序列化”(serialize)),然后把这个消息发送给服务端提供的方法,服务端的方法会从消息中解出序列化(deserialize)发来的参数,然后执行,最后仍以这一方式(当然这时发送方和接收方换了个位置)将方法的返回值(如果有的话)发送给客户端。

所以,任何RPC 机制都一定是IPC 机制(因为前者只是后者的一种特殊形式),但反过来却不一定是这样。正如我们在这一节中将要讨论和深究的那样,Android 中服务的调用模式是用RPC 方式实现的。下表对比了现代操作系统中使用的RPC 机制。

所有的RPC 机制都有一些共同特性:

  • 作用范围——表示RPC是否可以在远程和本地主机间进行,还是只能在本地主机中进行。
  • 索引目录——提供定位服务这一查询功能的服务程序。
  • 预处理模块——用来产生将参数序列化装入消息或从消息中解出序列化参数的代码的工具。
  • 通信信道——消息传递的媒介。

Android 应用的开发者可以幸福地忽略掉服务调用的底层实现方式。大多数Android 应用的开发者所熟悉的调用服务的方法是:他们只需调用Context 对象的getSystemService()方法,这个方法只需接收某个Android 系统服务的服务名作为输入参数,就能返回一个具体格式/含义不详的对象,通过这个对象就能得到指定的服务对象,并通过它调用服务的方法。

下图中展示的就是这个调用大多数服务方法的通用模式。该图已经做了某种程度的简化(例如,系统服务的句柄应该是会被缓存的),但仍足以展示整个过程。服务在被使用之前首先应该已经由某个server 进程(通常这个进程就是system_server,但也有可能是个第三方进程)通过调用android.os.ServiceManager 中的方法注册好了。我们回想一下,这个类提供了serviceManager 的一个Java 接口。

优点和缺点

Android 的系统服务架构遵循的是一个典型的本地客户端/服务端通信模式,它和其他操作系统(比如iOS)中使用的是一样的。虽然在iOS 中没有Binder——它使用的是它自己实现的一个被称为“Mach 消息”(Mach Message)的消息传递架构。在iOS 中,servicemanager 的角色(即端点映射器)是由launchd 进程扮演的,此外,这个进程同时还要扮演Android 中由/init完成的,传统linux 中PID 为1 的进程所饰的角色。

我们一眼就能看出这一架构的一个明显的缺点:过程间通信的开销实在是太大了,特别是在必须进行的序列化和解序列化消息的过程中,以及在交替切换进程时所必需进行的进程上下文切换时,开销尤为巨大。这一缺点确实会带来可观的性能损失。

既然有这么大的缺点,那么这一架构一定能带来更大的,或者至少是足以抵消这一缺点的好处,才会被采用,事实也确实如此:除了能让设计更为简洁之外,它还能做到权限隔离,这自然也能让客户端/服务端架构获得更高的安全性。在设计所依据的场景中,客户端进程应该是个不可信的用户App,它应该完全避免拥有任何权限,所以它想要完成任何操作,都必须完全依赖于服务调用。如果用户App 是用原生代码写的,这也就等于App 是运行在沙箱(sandbox)中的,即使在需要时,它也是不能(直接)访问设备或数据存储的(只能通过系统服务来完成这一操作)。事实上,这就是在iOS 中发生的情况[在iOS 中App 是被“关在监狱里的”(jailed),这也是术语“越狱”的由来],而在Android 中,情况也差不多,对于大多数进程来说,Android是根据文件系统中的访问控制权限来决定相关访问是否应该被拒绝的。

而另一方面,服务进程则是可信的,我们指望用它们进行所有的安全检查,在同意对相关请求提供服务之前,确认对应的客户端确实拥有权限。再说一遍,在这一点上Android 和iOS这两种相互竞争的系统的做法是一致的。只不过iOS 应用用的是(嵌入在二进制可执行文件的代码签名中的)entitlement,而Android App 用的是apk 安装包中的manifest 文件。在这两种情况下,权限的声明都是位于应用运行时的作用域之外的,即它们会在应用安装时被检验(或者在iOS 中,由苹果公司负责验证),而应用自身是无法修改它们的,特别是iOS 中使用的entitlement,它是(作为缓存的代码签名块的一部分)被存放在内核空间中的,而Android 中各个App 所拥有的权限则是由PackageManager 予以维护的。

序列化和Android 接口定义语言(AIDL)

在调用模式的设计的术语中,getSystemService()方法返回的对象只是一个“代理”(Proxy)。在这个对象内部记录着一个通过调用binder 获得的指向实际服务的引用(reference),而该对象导出的各个方法,在大多数情况下实际上也只是一些stub 容器而已,这些容器也被称为“Parcel”,其中存放的是被顺序打包(序列化)到Binder 消息里去的,需要传递给远程方法的各个参数。远程调用的各种方法及其参数就是以这一方式使用AIDL 序列化的。实际上AIDL 本身并不是一种真正的语言,它实质上只是一种能被aidl SDK 程序(在build 过程中,如遇到.aidl 文件时就会调用它)读懂的Java 衍生语言而已。aidl 能够自动生成把相关参数序列化打包到Binder 消息中去,并从返回的Binder 消息中提取出远程方法的返回值所需的Java 源码。这些代码被称为“样板文件”(boilerplate),即它可以根据定义文件自动生成,并保证编译得干净利落。.aidl 文件的样例如下所示。

.aidl 文件的一个例子

就像你看见的,.aidl 文件有点类似于头文件,其中只定义了方法(即可能存在的对象),但并没有给出它们的具体实现代码。

aidl 工具完成了一项几乎是不可思议的任务:向开发者隐藏了Android IPC 机制实现的细节。事实上这个任务完成得是如此漂亮,以至于大多数开发者竟然可以心安理得地忽略掉Binder 所扮演的角色,甚至可以完全无视它的存在。
高级用户依然可以把Binder 丢在脑后,特别是在拥有了像service 这样强大的工具后。使用service 我们就能在命令行中直接调用Android 服务提供的方法,如下面这个实验所示。

实验:使用service 命令调用服务

service 真正强大的部分在于:它能直接调用各个service 中的方法。
使用service call 调用一个方法其实也很方便:只需指定服务名及要调用的方法的序号(这个序号其实就是按各个方法在服务的.aidl 文件中的出现顺序,分配到的一个流水顺序号)即可。此外,根据被调用方法的具体定义,可能还需要输入一些被调用的方法的参数。service 这个命令行程序支持两种类型的参数——i32(用来传递一个int 型的变量)和s16(用来传递一个unicode 编码的字符串)。不过在实际使用过程中,int 型变量可以用来传递任何一种32 位的变量(比如float 型变量),而unicode 编码的字符串也可以用来传递任意一种对象。

所有用service list 命令列出的服务都有一个接口(在服务名后面的方括号中给出),我们可以用上述方法调用它们。在AOSP 源码中,每个服务都有一个对应的.aidl文件,服务的所有方法及其参数都明确地定义在这个文件中。有了这些定义,你就可以根据自己的需要调用其中任何一个方法——数出该方法的序号,并传给它对应的参数即可。下图中给出的是一些你可能感兴趣的服务的方法。

service call 命令

在不同版本的Android 之间(甚至是在同一个API 版本号的不同Android 之间),分配给各个方法的序号可能会发生变化,例如KitKat 中的IDisplayManager 和IPowerManager。尽管这种情况极少发生,但它还是会发生的,所以请小心为妙。一般而言,在程序中用这种直接写死的序号的方式调用方法并不是个好主意。如果你确实需要在工具或App 中使用这些私有API 的话,请用对应版本源码中的.aidl 文件,在编译过程中自动生成被调用方法的序号。

用这种方法调用一个方法,方法将会把返回结果放在一个Parcel(Binder 中用来称呼消息的术语)中返回来。每个Parcel 中至少含有一个32 位的返回值[用0x00000000 表示方法执行成功,其他的一些值表示不同的出错码。如果调用时输入的方法序号超出了规定的范围,通常会返回0xffffffff 或0xffffffb6(“not a data message”)[根据AIDL 中该方法返回值的具体定义,在这个32 位数后面还会跟一个int 型的数(i32)或一个二进制数据块[格式是一个表示数据块长度的数,再加上二进制数据块本身(通常这个二进制数据块中存放的是一个unicode 字符串,不过有时也会是其他类型的对象)]。因为service 和Binder 一样,对这样一个二进制数据块中放的是什么数据是一无所知的,所以它就没法向od 命令那样直接把返回结果显示出来,而是只能把Parcel 中的内容以十六进制的形式显示出来(边上再辅以这些数据的ASCII 含义)。

只有那些拥有(在方括号中给出的)公开接口的服务可以被调用。注意,也不是所有的服务都会盲目地让自己能够以这种方式被调用:根据安全策略,不同服务的安全策略都是不一样的,你所请求的服务可能会被拒绝。如果发生了这种情况,service call 命令的输出结果中会包含一个unicode 编码的出错消息,输出结果如下

service call 返回的出错消息

不过,一旦绕过了这些权限验证(比如你可以以root 身份运行程序),以这种方式使用servicecall 就能让你拥有近乎无限的能力——能够使用Android 框架服务提供的所有特性和功能。随着接下来一个个地介绍各种Android 框架服务,我们将向你展示这些服务在对应的.aidl 文件中的定义,以及服务中各个方法的序号。

读者评论

相关专题

相关博文

  • Kotlin 初体验:主要特征与应用

    Kotlin 初体验:主要特征与应用

    管理员账号 2017-08-15

    小编说:Kotlin 是一种针对 Java 平台的新编程语言。它简洁、安全、务实,并且专注于与 Java 代码的互操作性。它几乎可以用在现在 Java 使用的任何地方 :服务器端开发、Android 应用,等等。本文我们将详细地探讨 ...

    管理员账号 2017-08-15
    1354 0 0 0
  • 6种常用View 的滑动方法

    6种常用View 的滑动方法

    管理员账号 2017-08-01

    小编说:View 的滑动是Android 实现自定义控件的基础,实现View 滑动有很多种方法,在这里主要讲解6 种滑动方法,分别是layout()、offsetLeftAndRight()与offsetTopAndBottom()、...

    管理员账号 2017-08-01
    1085 0 0 0
  • #小编推书#快速高效地展炫酷动画效果

    管理员账号 2017-02-13

    小编说 目前,APP Store上的应用已经超过150万个,而纵观排名较为靠前的应用,无一例外都有着一个共同的特点,那就是良好的用户体验。动画作为用户体验中最复杂、最绚丽的技术已经备受开发人员和产品设计人员的重视。而如何将炫酷的动画...

    管理员账号 2017-02-13
    400 0 0 0