Service与Android系统设计(二)

Service与Android系统设计(2)

特别声明:本系列文章LiAnLab.org著作权所有,转载请注明出处。作者系LiAnLab.org资深Android技术顾问吴赫老师。本系列文章交流与讨论:@宋宝华Barry

共18次连载,讲述Android Service背后的实现原理,透析Binder相关的RPC。

1.     AIDL

由于Binder能够支持RPC,则基于代码有可能会变得异常复杂,于是,在实际的编程过程里,我们也还需要其他的辅助手段。比如,在实际的实现里,我们都会存在大量的RPC访问:

Service与Android系统设计(二)

在这种大量的RPC实现里,会有大量地处理RPC调用的重复代码,比如RPC的发送部分,Server端实现的IPC解析与分发部分。这些重复代码是没有意义的,而且在实际过程里,这种重复代码也将会是错误的源头。想像一下,如果上图所描述的RPC有100个,此时,我们将需要实现一个多大的switch()跳转。还有一个设计上的问题,当然我们使用固化的switch()来处理这种大量分支跳转,则我们的代码在设计上会被固化,我们不能灵活地重构我们的代码。于是,我们在实现Service时,我们先会使用Proxy模式来进行重构。标准的Proxy模式构成如下:

Service与Android系统设计(二)

对于同一Subject接口类,会被拆分成Proxy与具体实现的SubjectImpl类,方法的实现在SubjectImpl类里完成,而Proxy类所实现的则是将某些方法调用转发到SubjectImpl类。当客户端通过统一的Subject对象进行访问时,实际上是通过Proxy类来完成这转发。这样,接口访问与接口的实现则会隔离开,只通过基本的接口类Subject进行交互,从而降低了设计上的耦合性。在Android的跨进程调用里使用Proxy模式,则得到如下的示意图:

Service与Android系统设计(二)

Android里使用的Proxy模式,也是通过一个接口类IInterface派出来具体一个Proxy类和一个Stub类,由Proxy来提供访问接口的解析能力,而具体实现由Stub类来提供。于是,我们可以不再使用底层的IBinder来访问远程对象Service,而是通过一个抽象的接口类IInterface来进行访问。对应于每个IInterface抽象类,会派生出两个不同类,BpXXX和Stub,BpXXX用来提供Proxy功能,Stub类来提供具体实现。BpXXX,它的命名是Binder + Proxy + XXX的简写,XXX是指代具体的Service名字,比如Battery。通过这种模式重构成出来的实现就会将我们的接口的访问与实现完成的抽离开来,我们的应用程序进程里会各有一个BpXXX对象,而服务器里会有一个Stub对象,BpXXX与Stub通过IInterface来统一所能进行交互的接口方法,在应用程序里通过一个BpXXX,则可以访问到Service里实现的Stub对象的方法。于是,我们的服务器则只需要专注于方法的提供,通过继承Stub对象,然后实现IInterface接口类里定义的方法。

我们可以注意到,实际上,IInterface<XXX>接口、BpXXX与Stub,这三者在我们图中是被一个Framework的范围之内,当然最好这部分通过Proxy设计模式重构过的访问模式之后,重复性的代码都可以被通用化的代码实现,以减小工作。但这不现实,我们无法预测出某个被RPC化的对象会提供哪些方法,我们在系统里唯一可能比较明确的部分是IInterface这样的接口。这时,如果我们使用某种特殊工具,将IInterface对于接口访问方法(客户端与服务器所统一出来的远程调用方法)使用某种中间语言描述出来,这时,我们就可以得到我们想要减小重复代码的目的了,这样的工具就是IDL,在Android里被进一步简化成了Android版的 IDL,于是被称为AIDL。

IDL,全称是接口定义语言(Interface Definition Language),也是通过制定UML规范的OMG组织提出的交互的接口规范。IDL,是把RPC调用的实现,通过一种抽象的接口语言IDL定义出来,从面可以实现跨平台、跨语言、跨网络的调用环境。既然这种IDL语言只是一种规范,于是针对不同软件开发框架会提供不同的IDL解析工具,就好像HTML是一种规范,而浏览器与HTTP服务器可以有各种不同实现一样。几乎每种开发环境,都会构建出自己的一套IDL工具,从而实现不一样的交互性开发需求。比如我们最常用的可能会是Windows上的COM/DCOM接口,专用于Windows平台里的跨进程跨网络环境的开发;而像Mozilla或是OpenOffice这样软件环境里的IDL支持,IDL存在的意义更多地则是着眼于多语言支持,可以实现更加方便的插件开发,虽然也提供跨进程支持,但不是重点,更加不支持网络。最强大的IDL的应用环境CORBA(通用对象请求代理框架,Common Object Request Broker Architecture)则着眼于完整的跨平台计算环境的提供,从而支持多语言多平台跨网络,可以将异构型计算环境合并成统一计算环境,Java EE也会使用CORBA作为其网络交互的底层机制。

这些不同IDL工具所能起到的作用倒是很类似的,就是将标准的IDL语言,转化成自己平台开发语言的基类实现,然后再由调用端和实现端分别实现具体的接口。比如我们前面举的RPC的例子,如果使用IDL的C语言绑定,则会像是下面的这个样子:

Service与Android系统设计(二)

IDL文件里定义接口方法,xxx.idl文件里会定义接口RPCFunc1。而IDL通过特定IDL工具翻译之后,会生成针对客户端的xxxStub.h,和针对服务器端的xxxSkel.h,然后我们可以通过这两个头文件,会定义其具体所要求的客户端的RPCFunc1Stub()的实现和服务器端的RPCFunc1Skel()实现。实现完成的结果就是我们会得一个基于IPC的跨进程调用RPCFunc1(arg1,arg2)。当然,如果是有多语言支持,这时我们还会生成不同语言支持下的版本,比如在面向对象的语言环境里会生成xxxStub和xxxSkel基类,我们可以继承基类,然后再改写这特定的方法。

如果是在跨语言支持、跨网络、跨平台的环境里,IDL生成的客户端与服务器端实现还是会由于复杂的应用程序情境而需要我们自已在具体实现里进行定制,比如处理面向对象与面向过程之间、网络与单机环境之间、操作系统之间、大小端等诸多方面的差异性。而我们的Android则要简单得多,我们对于IDL的需求仅只是解决跨进程调用时的问题,只需要支持Java语言的绑定,只需要在Android环境里运行,只需要单机环境支持。于是,IDL本身,可以被大幅度简化,于是便有AIDL。

AIDL就是Android里的IDL,只提供Java环境支持,只针对Android环境,只应用于单机环境。引入了AIDL之后的Proxy模型,则几乎可以开始进行“傻瓜”式的RPC编程了。AIDL的作用如下图所示:

Service与Android系统设计(二)

针对于前面看到的Interface、Proxy、Stub三个类的代码重复问题,在AIDL里得到了很好的解决方案,有了AIDL之后,这三个类的定义是由AIDL工具转义自动生成的代码。我们在系统里会有.aidl文件,这是一种简化过的面向Java的IDL文件,在这一文件里定义调用方与被调用方所统一使用的接口方法。AIDL文件会通过AIDL编译工具编译成Interface、Proxy、Stub三个基类的定义,当然这三个类在Binder环境里会是,IInterface<XXX>,BpXXX,与XXX.Stub,这些是自动生成的,不需要修改。应用程序这端会通过Interface定义,直接访问远端的Stub,但实际上底层会通过BpXXX这个Proxy类来完成转发。而我们实现的部分,只需要在Service类里继承并实现Stub类的拓展接口即可。于是,我们便可以很方便地基于Binder得到了RPC的多进程交互的能力。

通过AIDL来编跨进程的Remote Service,是Android里提供的一种功能强大,但编写简单的一种编程模型。我们需要通过Java编程,透过Binder IPC来给系统里的其他部分,或是应用程序共享某些功能接口时,必须通过AIDL来进行编程。所以AIDL不光是对于应用程序的编程很重要,对于Android系统层开发也很重要,而AIDL在编程上并没有带来很大的开销,并不是特别复杂。我们先来看一下Android里的AIDL编程。 

1.1   使用AIDL申明远程接口

AIDL的语法其实很简单,基本上我们可以把它看成Java语言里一种类似于C头文件的格式,它只包括方法的定义,但不包含实现。它大部分是定义一个或者多个的接口方法,通过对这些接口方法的抽象定义,会定义方法的参数列表与返回值。这种定义方法的AIDL是AIDL解析时的入口,但为了支持数据类型的拓展,它还支持通过Parcelable接口类来拓展数据类型的支持。这是因为AIDL在语法上仅支持Java的基本数据类型和一些Android环境里的基本类,这样可以使用AIDL整个环境的实现和自动生成的代码都可以变得简单,AIDL不直接支持的数据类型,则需要通过引用其他的AIDL定义来导入进来。

AIDL支持数据类型:

  •  Java的基本数据类型(元数据类型,Primitive JavaProgramming Language Types),像int, boolean等。
  •  以下几种基本类:
    •    String
    •    List
    •    Map
    •   CharSequences
  •    通过AIDL导入其他AIDL文件里定义的内容,一般用于导入其他的接口类
  •    通过AIDL导入的其他Parcelable的接口类,用于导入AIDL所不能支持的数据类型
  •    AIDL只能用于定义方法,不能用于定义类结构,虽然也会导入Parcelable接口定义,但Parcelable只是描述存在某个类,并不会描述类的结构
  •    AIDL在参数上会使用in, out, inout三种描述符,主要是用于通过Binder传递时指定单向入,单向出,和双向传递。我们可以从后面的Parcel看到,在跨进程间传递数据还是有开销的,通过这种指定方式可以有更高效率。比如in、out会指定只在收发两端进行一次复制,inout则需要进行双向复制,从客户端复制到服务端,然后在调用完成后再从服务端发送回客户端。我们可以根据参数用途进行具体指定,如果不加指定,参数默认为in类型。

有了这样数据类型支持之后,实际上AIDL,可以应用于几乎任何情境。我们定义一个AIDL文件时,可以通过在Eclipse的应用程序工程里加入.aidl文件,将这一文件放在src/包名/的目录下,这样ADT工具会自动使用AIDL工具来编译这一文件,也可以手工添加这样的文件,然后使用Android.mk文件指定AIDL工作来进行编译。最后AIDL都会将编译结果放在gen目录里(跟R.java的保存位置一样)。

定义一个AIDL文件很简单,基本上可以认为是把Java定义里的属性与实现部分去掉,把得到的文件命名为.aidl即可。出于命名的规范性,一般我们把要需要抛出接口的类定义之前加大写的I(Interface的首字母),比如我们希望声明一个远程接口类TaskService,我们一般会定义一个叫ITaskService.aidl的文件。当然,既然aidl作用于Java,于是我们也需要指定包名,以限定该接口类的域名空间,这时跟Java语言一致,使用package加包名。

packageorg.lianlab.service

然后在这一行之后,通过一个interface定义我们需要抛出来的接口类,这一般会与我们.aidl文件名是一致的,比如我们前面的ITaskService.aidl,则我们会使用interface ITaskService{},然后再在ITaskService的作用域内,定义我们需要使用的方法。最后我们得到的.aidl文件,大概会是这个样子:

packageorg.lianlab.service;

interfaceITaskService {

    int getPid ( );

}

是不是很简单?其实对应于Java实现,因为aidl文件里只是定义接口类里的方法,于是不需要额外的引用,所以内容会很简单。因为我们现在需要的这两个远程接口方法,都只使用最基本的数据类型,如果是需要使用aidl支持的数据类型以外的类,则我们还会需要引用其他的aidl文件,将这些新的数据定义导入我们当前的aidl定义,这时我们会使用import语句来导入这样的aidl定义。

       所以综合一下,AIDL基本语法便是由package、import、interface这样格式构成,然后再在interface里定义所需要使用的方法。使用import来引用各个aidl文件里定义的接口类,则避免了接口类的重复定义,而通过import引用Parcelable接口类,则使AIDL能够很灵活方便地传递复杂数据结构。唯一需要注意的是,AIDL的解析工具在实现上很简单,并没有复杂的容错性检查,在编写AIDL时我们需要注意格式问题,比如intgetPid(void);则是不对的格式。

稍后来看到,我们怎么样针对这么简单几行的AIDL文件编写一个Remote Service,提供这个特殊的getPid()远程调用。