转载:http://blog.csdn.net/21cnbao/article/details/8086487
特别声明:本系列文章作者系吴赫。分多次连载,讲述Android Service(JAVA Service、Native Service等)背后的实现原理,透析基于Binder的RPC以及Linux Binder驱动。
Service在Android应用程序里四大实体之一。Android的应用程序不光是需要有图形界面来进行交互,有时也会需要在没有交互的情况下进行的操作,比如下载、更新、监听等。比如目前对我们网络生存影响如此之大的社交网络、或是更老一些聊天工具,总需要这类应用程序可以一直在后台运行,以等待可能过来的消息。:
Service拥有一部分Activity所无法完成的能力。一是后台运行,有时我们并不希望有过多对话框来影响用户体验,开机自动启动,便可默默地在后台运行。另一特性,就是不被Activity生命周期所管理,Activity处于完全活跃的周期是onResume()与onPause()之间,如果这周期之外发生了事件,实际上Activity构成的执行部分也不会被执行到,从而无法响应处理,但Service由于本身过于简单,则会通过一定的辅助手段来达到这个目标。
从Android应用程序的设计原理来看,Service同样也是主线程里执行的(这点尤为重要,Service由于在主线程里执行,于是也会因为执行耗时操作而触发ANR)。一个应用程序既然可以通过拓展Activity得到可交互的代码逻辑,同样也可以通过拓展Service来得到不交互在后台执行的逻辑。如下图加强部分所示:
Activity对应用程序来说是最重要的组件,但从Android系统设计的角度来看,Service对系统层实现来说才最重要的。Service是构建系统的根本,支持整个系统运营的环境framework,本身就是由大量Service来构成的。也就是说,Service反而是构建Activity的基础环境。
Android与其他系统设计最大的不同之处在于,它并不是一种传统的系统环境,而是一种更加开放的系统。传统的图形操作系统里,会有基本环境,会有系统管理组件,应用程序只是作为补充性功能实现。但Android并不如此,Android系统是没有应用程序的,达到了“无边界”系统设计的最高境界,“手里无剑,心中有剑”。整个Android系统的设计使自己扮演着支撑系统的运行环境的角色,不再有基本系统的概念,而变成了一种“有或者无”的应用程序的支撑环境,没有系统组件的概念。而我们所谓的系统应用程序,我们只能称它们为“内置”应用程序,只是服务于黑心移动运营商的一种方式而已。
这种设计的精髓在于,系统本身不会处理交互,而只是提供交互的手段。从前面我们对于应用程序运行环境的分析中,我们可以看到,Android的Framework,提供一部分功能供应用程序调用,而除了这些应用程序直接使用的API实现,其他代码逻辑就会全是由Service构成。当然作为系统实现角度的Service,与应用程序编程里实现的Service是有差别的,更强调共享,但基本构架一样。在过渡到Android系统的解析之前,我们先从应用程序的Service概念入手。
我们先来在应用程序里写一个简单的Service。打开Eclipse,新建一个Android工程,然后再创建一个新的基于Service基类的类。与Activity的编程方式类似,Service在编程上也是基于回调方式实现的,我们继承基类Service之后所需要做的,就是通过IoC模式替换原来的Service回调的实现:
- import android.app.Service;
- import android.content.Intent;
- import android.os.IBinder;
- import android.util.Log;
- public class LianlabServiceextends Service
- {
- private staticfinal String TAG ="LianlabService";
- @Override
- public void onCreate() {
- super.onCreate();
- Log.v(TAG, "inonCreate()");
- }
- @Override
- public int onStartCommand(Intent intent,int flags,int startId) {
- super.onStartCommand(intent, flags, startId);
- Log.v(TAG, "inonStartCommand()");
- return START_STICKY;
- }
- @Override
- public void onDestroy()
- {
- Log.v(TAG, "inonDestroy().");
- super.onDestroy();
- }
- }
有了Service的具体实现之后,系统并不会自动地识别到这一实现,在Android世界里,一切都通过AndroidManifest.xml来驱动,于是,我们还需要修改AndroidManifest.xml文件,加入Service的定义:
- <applicationandroid:labelapplicationandroid:label="@string/app_name">
- <serviceandroid:nameserviceandroid:name=".LianLabService"/>
- ;/application>
在上面这种方式里实现的Service,可被执行的方式很有限,就是提供一个可执行的线程环境,可以被Intent所驱动,执行onStartCommand()回调。功能有限并不代表无能,在Android系统里,我们可能还经常会碰到这样的需求:比如我们使用GPS里来记录我们行动轨迹时,这时我们很可能需要通过后台的执行的代码来定时检查GPS的定位信息;杀毒或是监控软件可能希望驻留在后台,并可被Intent来驱动开始进行杀毒;我们的聊天或是社交应用,需要在后台定时地与服务发送“心跳”(Heart beat),用来标识自己的在线状态等。这样的例子,大家可以回头到我们画的GPS轨迹跟踪的构成示意图,这样的跟踪软件,必须是通过一个接收启动完成信息的Broadcast Receiver来监听自己是否应该被执行,而接收到到启动完成的Broadcast Intent之后,则必须触发一直在后台运行的TrackerService的执行。
既然我们在上述方式里实现的Service是由Intent驱动的,于是我们的使用这一Service部分的代码也会很简单。在任何可被执行到的代码里使用startService(Intent)就可以完成,我们可以给某个控件注册点击事件支持onClickListener对象,然后覆盖onClick()回调方法:
我们这里既然使用到了Intent,也就是说我们还可以通过extras这个Bundle对象给我们这里实现的LianLabService来传递运行的参数。于是,这时我们的代码貌似有了pthread多线程执行效果,通过传参,然后我们会执行一个在另一线程里运行的函数,只是函数是固定的onStartCommand()回调方法。但这只是貌似,并非实际情况,Service的执行与后台线程方式极大不同,Service只是一种代码逻辑的抽象,实际上它还是运行在Activity同一线程上下文环境。
于是,我们并不能用Service来进行任何耗时操作,否则会阻塞主线程而造成应用程序的无法响应错误,也就是臭名昭著的ANR错误。Service仅能用于不需要界面交互的代码逻辑。
这种使用Intent来驱动执行的Service,可用性有限,并不能完全满足我们对于后台服务的需求。对于后台执行的代码,我们更多的应用情境不光是希望进行后台操作,我们可能还希望能进行交互,可以随时检查后台操作的结果,并能暂停或是重启后台执行的服务,可以在使用某一Service时保证它并不会退出执行,甚至一些提交一些参数到后台来进行复杂的处理。这时,我们可以使用Service的另一个访问方式,使用Binder接口来访问。我们的Service基类还提供这类应用的回调方式,onBind()、onUnbind()和onRebind()。使用Binder来访问Service的方式比Intent驱动的应用情境更底层,onBind()回调主要用于返回一个IBinder对象,而这一IBinder对象是Service的引用,将会被调用端用于直接调用这一Service里实现的某些方法。
同样的Service实现,如果通过IBinder来驱动,则会变成下面的样子:
- import android.app.Service;
- import android.content.Intent;
- import android.os.IBinder;
- import android.util.Log;
- public class LianlabServiceextends Service
- {
- private staticfinal String TAG ="LianlabService";
- @Override
- public void onCreate() {
- super.onCreate();
- Log.v(TAG, "inonCreate()");
- }
- @Override
- public intonStartCommand(Intent intent,int flags,int startId) {
- super.onStartCommand(intent, flags, startId);
- Log.v(TAG, "in onStartCommand()");
- return START_STICKY;
- }
- @Override
- public void onDestroy()
- {
- Log.v(TAG, "inonDestroy().");
- super.onDestroy();
- }
- finalIService.Stub m_binder =newIService.Stub() {
- ...
- }
- @Override
- public IBinderonBind(Intent intent) {
- Log.v(TAG, "inonBind().");
- return mBinder;
- }
- @Override
- public booleanonUnbind(Intent intent) {
- Log.v(TAG, "inonUnbind().");
- return mAllowRebind;
- }
- @Override
- public void onRebind(Intentintent) {
- Log.v(TAG, "inonRebind().");
- }
- }
使用IBinder对象来触发的Service,在访问时的代码实现则变得完全不样了。比如我们同样通过onClick()来操作后台的某些操作,但这时并非通过Intent来完成,而是直接使用某个引用这一Service的IBinder对象来直接调用Service里实现的方法。
如果Service里实现了某些方法,比如kill(),在上述代码之后,我们对Service的驱动则会变成代码上的直接调用。在onServiceConnected()回调方法被触发之后,我们始终都可以通过m_service.kill()来访问Service里的kill()方法。而bindService()这方法的调用,则会触发onServiceConnected()事件。
这样就要让人抓狂了,既然如此麻烦,何不直接调用呢?所以,事实上,这里列举的这种代码实现方式,在现实编程里确实不常用。一般而言,如果Service通过IBinder对象来触发,那只会出于一个理由,提供一种可能性,将来可以更灵活地提供给另一进程来访问,这就是我们稍后会说明的Remote Service。
这两种不同的Service的实现方式,将决定Service的不同被调用方式,或者准确地说,将决定Service的不同生命周期。
如图所示,Service的进程存活期是处理onCreate()与onDestroy()回调方法之间。onStartCommand()回调是不受控的,每次Intent都将触发执行一次, onStartCommand()执行完则会退出;而使用onBind()来驱动的Service,其活跃区间是onBind()与onUnbind()之间,其活跃周期始终在控制范围内。
得益于Android的进程间模型,无论是系统实现端的开发,还是应用程序的开发者,可认为自己的代码都将在一种安全的环境下执行。但对于在需要共享的场合,又带来了不方便之处,我们不再可以在一个进程里很方便地调用到另一进程里的实现。在上面两种Service实现里,基本上只能自已服务自己,而不能将功能共享给更多地使用者。于是,Android在设计初期,便引入了另一个概念,Remote Service。
Remote Service定义了如何从一个进程,直接访问到另一个进程里实现的方法,这样的机制在通用编程领域被称为RPC,Remote Procedure Call,在Android环境里,因为这样的RPC与Service的概念揉合到了一起,所以被称为Remote Service。
如下图所示,Remote Service基本上算是在Bounded Service实现上的一种拓展。调用端(一般是与用户交互的Activity)会通过bindService()发一个Intent到Service进程,而Service进程会在onBind()回调方法里返回自己的Service处理接口。当调用端得到相应的处理接口,也就是下图所示的Stub.Proxy对象之后,就可以调用远端Stub对象里实现的方法。
当然,由于这种方式需要在每一次调用时都进行一次远程调用,于是实现起来并不简单,甚至可以说是会很麻烦。在上图逻辑里要实现一个Remote Service,在这一Remote Service里实现一个方法,就需要加入多个对象。于是,在Android里引入了AIDL,可以通过AIDL来简化通用代码的处理,在上图中Stub对象、Stub.Proxy对象都将由aidl自动管理。
实现一个Remote Service,基本上可以分三步完成:
有了AIDL定义之后,我们便可实现这这一接口类的定义,主要是提供一个Stub类的实现,在这个Stub对象里,提供getPid(void)的具体实现。
于是,当应用程序通过某种方式可以取得ITaskServiceStub所对应的IBinder对象之后,就可以在自己的进程里调用IBinder对象的getPid(),但这一方法实际上会是在别一个进程里执行。虽然我们对于执行IBinder的Stub端代码的执行环境并没有严格要求,可以在一个运行中的进程或是线程里创建一个Stub对象就可以了,但出于通用性设计的角度考虑,实际上我们会使用Service类来承载这个Stub对象,通过这样的方式,Stub的运行周期便被Service的生存周期所管理起来。这样实现的原因,我们可以回顾我们前面所描述的功耗控制部分。当然,使用Service带来的另一个好处是代码的通用性更好,我们在后面将感受到这种统一化设计的好处。
于是,对应于.aidl的具体实现,我们一般会使用一个Service对象把这个Stub对象的存活周期“包”起来。我们可以通过在Service里使用一个 final类型的Stub对象,也可以通过在onBind()接口里进行创建。这两种不同实现的结果是,使final类型的Stub对象,类似于我们的Singleton设计模式,客户端访问过来的总会是由同一个Stub对象来处理;而在onBind()接口里创建新的Stub,可以让我们对每个客户端的访问都新建一个Stub对象。出于简化设计的考虑,我们一般会使用final的唯一Stub对象,于是我们得到的完整的Service实现如下:
有了这样的定义之后,我们还需要在AndroidManifest.xml文件里将Service声明出来,让系统里其他部分可以调用到:
在AndroidManifest.xml文件里,会在<application>标签里通过<service>标签来申明这一应用程序存在某个Service实现,在service命名里,如果service的名称与application名称的包名不符,我们还可以使用完整的“包名+类名”这样命名方式来向系统里注册特殊的Service。在<service>标签里,我们也可以注册<intent-filter>来标明自己仅接收某一类的Intent请求,比如我们例子里的action会匹配“org.lianlab.services.ITaskService”,如果没有这样的intent-filter,则任何以TaskService(ComponentName的值是”org.lianlab.services.ITaskService”)为目标的Intent请求会触发TaskService的onBind()回调。当然,我们在<service>标签内还可以定义一些权限,像我们例子里的这个TaskService是没有任何权限限制的。有了这个AndroidManifest.xml文件,我们再来看看客户端的写法。
在客户端进行访问时,由于它必须也使用同一接口类的定义,于是我们可以直接将同一.aidl文件拷贝到客户端应用程序的源代码里,让这些接口类的定义在客户端代码里也可以被自动生成,然后客户端便可以自动得到Proxy端的代码。因为我们这里使用了Service,又是通过onBind()返回IBinder的对象引用,这时客户端在使用IBinder之前,需要通过bindService()来触发Service端的onBind()回调事件,这时会通过客户端的onServiceConnected()回调将Stub所对应的Binder对象返回。我们在稍后看AIDL的底层实现时会发现,在此时客户端的Binder对象只是底层Binder IPC的引用,此时我们还需要创建一个基于这一Stub接口的Proxy,于是在客户端会需要调用asInterface()创建Proxy对象,这一Proxy对象被转义成具体的Service,在我们的例子里,客户端此时就得到了ITaskService对象。从这时开始,在客户端里通过ITaskService.getPid()的调用,都会通过Binder IPC将操作请求发送到Service端的Stub实现。于是,我们可以得到客户端的代码,在Android里我们一般用Activity来完成这样的操作,如下代码所示:
这种方式很简单易行,实际上在编程上,AIDL在编程上带来的额外编码上的开销非常小,但得到了灵活性设计的RPC调用。
在AIDL编程环境里实际上是支持反向调用的,原理跟我们实现一个Remote Service一样,就是通过把Proxy与Stub反过来,就得到了这样的回调式的aidl编程。唯一的区别是,当我们的Stub在Activity时实现时,我们实际上跟后台线程执行也没有区别,Callback并非是在主线程里执行的,于是不能进行重绘界面的工作。于是,我们必须像后台线程编程一样,使用Handler来处理界面显示处理。
前面我们说过aidl是可以互相引用的,于是我们可以借用这样的机制,通过引用另一个新增的aidl文件来加强我们前面的单向的TaskService版本。我们先增加一个新的ITaskServiceCallback.aidl文件,与ITaskService保持同一目录:
在这一定义里,我们新增加了一个ITaskServiceCallback的接口类,基本上与我们前面的ITaskService.aidl一样,在这个接口类里,我们新加了一个valueCounted()方法,这一方法将会被Service所使用。
在这个文件里,唯一与ITaskService.aidl不同之处在于,我们使用了一个oneway的标识符,oneway可以使aidl调用具有异步调用的效果。在默认情况下,基于aidl的调用都会等待远端调用完成之后再继续往下执行,但有时我们可能希望在跨进程调用会有异步执行的能力,我们在发出调用请求后会立即返回继续执行,调用请求的结果会通过其他的callback返回,或是我们干脆并不在乎成功与否,此时就可以使用oneway。当然,从我们前面分析aidl底层进行的工作,我们可以知道,所谓的远程调用,只不过是通过Binder发送出去一个命令而已,所以在aidl里面如果使用了oneway限定符,也就是发送了命令就收工。
然后,我们修改一下我们的ITaskService.aidl,使我们可以使用上这个新加入的回调接口:
我们会引用前面定义好的ITaskServiceCallback.aidl文件,通过包名+接口的方式进行引用。为了省事,我们直接在原来的getPid()方法里进行修改,将新定义的ITaskServiceCallback接口类作为参数传递给getPid()接口。于是,在Service端Stub对象里实现的getPid()方法,将可以使用这一回调对象:
加入了回调之后的代码结构并没有大变,只增加了3部分的内容,通过这三部分的内容,我们此时便可以记录我们的getPid()总共被调用了多少次。
加入了回调之后,对Service端的实现并没有增加多大的工作量,因为作为回调,实现是放在客户端上来完成的。
因为我们的aidl接口已经发生了变动,于是需要将新加的ITaskServiceCall.aidl与改变过的ITaskService.aidl文件拷贝到应用程序工程里。我们再来看一下客户端实现代码需要作怎样的调整:
新加入的回调接口对象,也没给我们带来多大的麻烦,我们最重要是提供一个实例化的ITaskServiceCallback.Stub对象,然后通过getPid()将这一远程对象的引用发送给Service端。之后,Service处理后的回调请求,则会通过Binder会回到客户端,调用在Stub对象里实现的回调方法。所以我们实际增加的工作量,也仅是写一个ITaskServiceCallback.Stub接口类的实现而已:
从上面的代码可以看出,我们通过aidl创建回调方法好像比我们直接通过aidl写一个Remote Service还要简单。事实上,并非回调创建方便,在原则上,我们本只需要一个Stub对象便可以得到我们想要的RPC能力了,只不过出于管理存活周期的需要,才融入到了Service管理框架里,因为这种Service使用上的需求才带来了一些编程上的开销
aidl工具的工作原理也很简单, aidl工具的源代码在frameworks/base/tools/aidl里,如果对通过bison来实现编译器感兴趣也可以参考其实现。
而AIDL工具所完成的工作,是将aidl文件转义成一个通用的Java文件,我们实现的内容,便是拓展自这一Java文件里的定义。aidl工具生成的结果,一般与aapt工具生成的结果放在同一目录,在应用程序环境里,aidl生成的结果是一个在gen/包名/目录里与aidl文件前缀名相同的Java文件,我们的例子里会是gen/org/lianlab/services/ITaskService.java,我们可以看一下这个生成文件的内容:
上述生成的ITaskService.java文件,在类图上大致由如下的关系来构成:
IInterface接口类在Binder框架里是两边访问的基础,只通过asBinder访问一个IBinder对象。在Proxy模式里核心接口类Interface部分的功能,实际上则由拓展IInterface的自定义的ITaskService来完成,通过同一接口,客户端会得到Proxy对象,而Service会得到Stub对象。在Android环境里,Proxy只是Stub类的一部分,这是Proxy在实现上的一种变体,从而强调Stub作为实现上的重要性,Proxy需要依赖于其对应的Stub接口的存在。我们可以详细分析一下,这些自动生成的代码的含义:
总而言之,这些代码,就将客户端与Service端的代码,分别通过Stub与Stub.Proxy两个类来创建,通过这两个对象的引入,无论是调用RPC,还是提供RPC实现,都变得只是拓展一下原有的基本实现即可。这时就基本上达到我们前面描述过的基于Binder进行RPC通信的需求:
而在远程交互的实现上,我们基于AIDL的Remote Service,就会以如下的形式进行交互:
在编译阶段,idl文件里定义的接口会通过aidl工具翻译得到一个ITaskService.java文件,保存到gen供应用程序引用。这一ITaskService.java文件里会具体实现发送请求的Proxy接口类与提供实现并将执行结果返回的Stub接口类。在发生aidl调用时:
当然,此时我们得到了基本的跨进程调用方法的能力,这跟我们普通的编程模型一致了,像我们的一般的编程,都是从一个main()函数开始,调用不同的代码逻辑,最终得到一个大的复杂的可执行程序。但这并非全部,在现实的编程环境里,我们还需要实现一些回调式的编程,主要用于一些直接调用不太合适的情境。比如我们需要在后台做某些比较耗时的操作,像下载、登录等,如果直接调用再取返回值,此时,我们得到的结果就很悲惨,会需要等待函数执行完成之后才能继续执行,需要等待很长时间。在Android的单线程编程模型里,这种情况会更严重,我们Activity运行在主线程里,如果通过aidl调用到一个耗时操作,就会阻塞到主线程的执行,从而产生ANR错误。
这种方式实现的,只是单向调用,虽然我们也可以在发起调用的部分取得返回值,但只是一种单向的函数式调用。如果我们需要实现双向调用怎么做呢?我们不光会调用到远程代码里,也可以提供一种回调接口,让被调用方回调回来,就像是C语言里的回调函数,这样,远程调用的世界就完整了,见后续内容。
我们可以先来了解一下RPC的实现。
跨进程访问,实际上并非Android环境才需要,这是所有跨进程软件设计里的必须项。这种交互性的跨进程需求,跟我们传统的C/S(客户端/服务器)构架类似,客户端使用IPC访问服务,而服务器端则实现具体的代码逻辑,通过IPC提供服务。如下所示
进程1提供客户端功能,而进程2提供服务器功能,在进程1里调用RPCFunc(1,2),实际上会触发到进程2里的RPCFunc1Impl1(1,2)的执行。
这样,从进程1的代码上来看,好像是完成了一次从RPCFunc1()到RPCFunc1Impl()的远程过程调用(RPC),但在内容实现上,则是经历了1-6这样6个步骤的串行操作。之说以说是串行,因为这6个步骤会顺序进行,进程1的执行RPCFunc1()这一函数时,直到第6步执行完成之前,都会被阻塞住。
通过通过串行实现后的这种特殊C/S框架,因为跟我们传递的函数调用类似,只是提供了跨进程的函数调用,于是根据这样的行为特征,我们一般会叫它们为远程过程调用(Remote Procedure Call,简称RPC)。支撑起RPC环境的是IPC通信机制,我们也知道套接字(Socket)也是IPC机制一种,而TCP/IP是Socket机制的一部分,于是很自然的,RPC也天生具备跨网络的能力,在实际的部署里,RPC一般会是网络透明的,通过RPC来进行访问的那端,并不会知道具体实现RPC的会是本地资源或是网络上的资源。RPC是实现复杂功能的基础,特别是一些分布式系统设计里,比如我们Linux环境里的网络文件系统NFS、AFS等,都是基于RPC,还有更高级一点,像我们的Corba环境、J2EE环境和WebService,都会使用RPC的变种。
拥有了RPC通信能力之后,我们在编程上的局限性便大为减小,我们的代码可以很灵活地通过多进程的方式进行更安全的重构,而且还可以进行伸缩度非常良好的部署。这点对于Android来说,尤为重要,因为我们的Android系统就是构建在基于多进程的“沙盒”模型之上的。在Android环境里,不存在基于网络来进行RPC的需求,而是使用高性能的面向对象式的Binder,于是,我们的RPC,需要通过RPC来构建。于是,简单的RPC通信流程,在Android系统里,则可以通过IBinder对象引用来完成,得到如下的实现逻辑:
我们会通过IBinder,来实现从一个进程来访问另一个进程的对象,得到远程对象的引用之后,虽然在进程1里我们像是真正通过这一IBinder来访问远程对象的某些方法,比如doXXX()方法,但实际上后续的执行逻辑则会被转到Binder IPC来发送访问进程,进程1在进入doXXX()方法之后,就会进入IBinder是否有返回的检测循环。当然此时由于IBinder设计上的精巧性,此时进程实际上会休眠到/dev/binder设备的休眠队列里。而提供RPC的进程2则相反,它启动后会一直在IPC请求上进行循环监听,当有IPC请求过来之后,则会将doXXX()的访问请求解析出来,访问这一方法,在访问完成之后,再将调用doXXX()的结果通过IBinder返回给发出调用请求的进程1。这时,会唤醒进程1继续往下执行,从IPC上取回调用的返回值,然后再执行doXXX()之后的代码。
通过这种RPC机制,在Android系统里,就可以更灵活地来设计交互过程,更方便地在多进程环境里进行低耦合化设计。在RPC交互的Server端的实现,可以灵活地根据自己的实现或是调用上的需求,开放出来一部分的自己实现的接口,从而给多个Client端提供服务。
由于Binder能够支持RPC,则基于代码有可能会变得异常复杂,于是,在实际的编程过程里,我们也还需要其他的辅助手段。比如,在实际的实现里,我们都会存在大量的RPC访问:
在这种大量的RPC实现里,会有大量地处理RPC调用的重复代码,比如RPC的发送部分,Server端实现的IPC解析与分发部分。这些重复代码是没有意义的,而且在实际过程里,这种重复代码也将会是错误的源头。想像一下,如果上图所描述的RPC有100个,此时,我们将需要实现一个多大的switch()跳转。还有一个设计上的问题,当然我们使用固化的switch()来处理这种大量分支跳转,则我们的代码在设计上会被固化,我们不能灵活地重构我们的代码。
于是,我们在实现Service时,我们先会使用Proxy模式来进行重构。标准的Proxy模式构成如下:
对于同一Subject接口类,会被拆分成Proxy与具体实现的SubjectImpl类,方法的实现在SubjectImpl类里完成,而Proxy类所实现的则是将某些方法调用转发到SubjectImpl类。当客户端通过统一的Subject对象进行访问时,实际上是通过Proxy类来完成这转发。这样,接口访问与接口的实现则会隔离开,只通过基本的接口类Subject进行交互,从而降低了设计上的耦合性。
在Android的跨进程调用里使用Proxy模式,则得到如下的示意图:
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语言绑定,则会像是下面的这个样子:
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的作用如下图所示:
针对于前面看到的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编程。
AIDL的语法其实很简单,基本上我们可以把它看成Java语言里一种类似于C头文件的格式,它只包括方法的定义,但不包含实现。它大部分是定义一个或者多个的接口方法,通过对这些接口方法的抽象定义,会定义方法的参数列表与返回值。这种定义方法的AIDL是AIDL解析时的入口,但为了支持数据类型的拓展,它还支持通过Parcelable接口类来拓展数据类型的支持。这是因为AIDL在语法上仅支持Java的基本数据类型和一些Android环境里的基本类,这样可以使用AIDL整个环境的实现和自动生成的代码都可以变得简单,AIDL不直接支持的数据类型,则需要通过引用其他的AIDL定义来导入进来。
AIDL支持数据类型:
有了这样数据类型支持之后,实际上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文件,大概会是这个样子:
是不是很简单?其实对应于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()远程调用。