特别声明:本系列文章LiAnLab.org著作权所有,转载请注明出处。作者系LiAnLab.org资深Android技术顾问吴赫老师。本系列文章交流与讨论:@宋宝华Barry
共18次连载,讲述Android Service背后的实现原理,透析Binder相关的RPC。在aidl环境里使用Parcelable接口,相对来说会更简单。我们前面也强调过,在aidl工作的实现上,为了达到简洁高效的设计目标,aidl只支持基本的数据类型,一个接口类在aidl环境里仅会定义方法,而不会涉及属性定义,所以这种限制作用到Parcelable,Parcelable在aidl会只是简单的一行,其他aidl使用到这些Parcelable接口的部分,可以直接引用定义这一Parcelable的aidl文件即可。
基于我们前面的例子来拓展一个Parcelable的例子,假如我们希望在进程能够传递一些进程间的描述信息,我们会把这样的数据组织到一个类时进行传输,我们可以把这个类叫TaskInfo,于是在单进程环境里,我们大致会有这样一个类:
我们通过这一个类来创建一个对象时,这个对象的有效使用范围只是在一个进程里,因为进程空间是各自独立的,在进程空间里,实际上这一对象是保存在堆空间(Heap)里的。如果我们希望通过IPC机制来直接传递这么一个对象也是不现实的,因为在Java环境里对象是通过引用来访问,一个对象里对其他对象,比如访问TaskInfo类的packageName,则是会通过一个类似于指针的引用来访问。
所以,通过IPC来复制结构比较复杂的对象时,必须要通过某种机制可以将对象拆解成一种中间格式,通过在IPC里传输这种中间格式然后得到对象在进程之间互相传递的效果。无论是使用什么样的IPC机制,这种传输过程都是会是一边把数据写进去,另一边读出来,根据我们前面的分析,我们使用一个继承自Binder的对象即可完成这种功能。但这种读写两端的代码会不停重复,也容易引起潜在的错误,Android系统会使用面向对象的技巧来简化这种操作。与进程间方法的相互调用不同,在进程间传递对象不在乎交互,而更关注使用时的灵活性。在对象传递时,共性是传输机制,差异性是传输的对象构成,于是我们在Binder类之上,再派生出一个Parcel类,专用于处理属于共性部分的数据传递,而针对差异性部分对象的构成,则通过一个Parcelable的接口类,可以通过这一接口将对象组成上的差异性通知到Parcel。
有了Parcel类与Parcelable接口类之后,我们要传递像TaskInfo对象则变得更容易。我们可以让我们的TaskInfo类来实现Parcelable功能,使这一类生成的对象,都将在底层通过Parcel对象来完成底层的传输。我们前面定义的TaskInfo类,则会被变成如下的样子:
通过上述的这一个类的定义,我们便得到了一个可以被跨进程传输的类,通过这个类所创建的对象,可以无缝地在进程进行传输。如果需要使用这一我们已经定义好的 Parcelable类,我们只需要新建一个aidl文件,描述这一Parcelable的存在信息,从而通知到aidl编译工具。如果是基于我们前面例子里定义的Parcelable类TaskInfo,我们会需要使用一个与之同名的,叫TaskInfo.aidl的文件:
package org.lianlab.services;
parcelableTaskInfo;
这一文件很简单,实际就是一行用于定义包名,一行定义存在一个实现了Parcelable接口的TaskInfo类。然后,我们在需要使用它的部分再将这一aidl文件导入,我们可以在前面的ITaskService.aidl进行拓展,定义一个新的getTaskStatus()接口方法来返回一个TaskInfo对象。在这些定义接口方法的aidl文件里使用Parcelable类很简单,只需要引用定义这一Parcelable类的aidl文件即可:
当然,这时我们的ITaskService接口变掉了,于是我们会需要改写我们的Stub类的实现,我们需要到TaskService.java的实现里,新增强getTaskStatus()方法的实现:
在我们新增的getTaskStatus()方法里,我们会创建一个新的TaskInfo对象,然后根据当前的进程上下文环境给这一TaskInfo对象进行赋值,然后再返回这一对象。由于TaskInfo现在已经荣升成Parcelable对象了,于是这一返回,实际上会发生跨进程环境里,在Remote Service的调用端返回这一对象。
从上面的代码示例,我们大致也可以分析到通过Parcel来传递对象的原理。这跟我们中寄送由零部件组成的物品类似。生活中,我们寄运由零部件构成的物品,一般是把东西拆散成零组件,于是好包装也方便运输,把零部件尽可能灵活摆放塞进一个盒子里,再寄送出去。接收到这个包裹的那方,会从盒子里将零部件拆散开来,再按拆卸时同样的构架再将零部件组装到一起,于是我们就得到了原来的造型各式的物品。出于这样的类比性,于是我们的负责搬运的类叫Parcel,就是包裹的意思,而用于搬运的拆装过程,或者准确的是说是可拆卸然后再组装的能力,就叫称为Parcelable。
对于Parcel的传输,0xLab的黄敬群先生(JServ)作了一个形象的比喻,希望通过传真机来发送一个纸盒子到远端。传真机不是时空传送带,并不会真正可以实现某个物品的跨空间传递,但我们一定的变通来完成:
我们可以把一个纸盒子拆解摊开,这时得到一个完全平面化的带六个格子的一个图形。我们通过传真机传送时就有了可能,我们传真这个带六个格子的图形到远端,这时远端的传真机就会收到同是六个格子的传真图像,打印到一张纸上。然后我们再将这张纸裁减开来,重新粘贴到一起,于是也可以得到一个纸盒子,跟我们原始的纸盒有着一模一样的外观。
于是,在发送时,我们大体上可认为是把对象进行拆解打包,然后塞进Parcel对象里。Parcel此时相当于容器的作用,于是其内容一定会有一段buffer用于存放这种中间结果。于是这一过程就会是通过writeToParcel()方法,将对象的属性分拆开,填写到这个Parcel的buffer里:
因为这一过程,在计算处理上相当于把原有来非线性存放的对象,通过平整化转移到一个线性的内存空间里进行保存,于是这一操作被称为平整化”flatter”。从前面的代码我们也可以看到,所谓的平整化操作,就是通过writeToParcel()写入到一个Parcel对象里。我们更细一点来观察的话,这一过程就是通过Parcel对象的writeInt()、writeLogn()、writeString()等不同操作方法,将对象的属性写入到Parcel的内置buffer里。
在读取的那一端,我们会通过某种手段,将Parcel对象读出来,通过CREATOR里定义的初始化方法,得到Parcel里保存的对象:
对应于发送时的flatten,我们接收时的操作就是unflatten,将对象从一个线性化保存在Parcel的内置buffer里的数据,还原成具体的对象。这一步骤会是通过CREATOR里通过Parcel来初始化对象的createFromParcel()来完成。
到此,我们至少就得到通过一个中间区域来搬运与传送对象的能力。对象不能直接拷贝与传输,但线性化内存是可以的,我们可以把内存从0到结果的位置通过某种IPC机制发送出去,在另一端进行接收,就可以得到这段内存的内容。最后,如果我们把Parcel对象的这段内容通过Binder通信来进行传输,此时我们就得到了进程间对象互相传送的能力:
我们在进行跨进程的对象传递过程里,都通过Parcel这样的包装来进行传输。所以只要我们的Parcel足够强大与灵活,我们可以进程间传递一切。我们的例子里仅使用Int、Long、String等基本类型,实际上Parcel的能力远不止如此。Parcel支持全部的基本数据型,同时因为Parcelable.Creator<T>模板同时支持newArray()与createFromParcel()两种接口方法,于是在传输时可以将两者结合,通过newArray()进行对象创建,同时通过createFromParcel()再依次初始化,于是Parcel自动会支持数组类型的数据。而对[]操作符的支持,使在Parcel对象里实现List、Map等类型非常容易。出于对传输时的节省存储与拷贝时的开销,于是在Array之上来提供了对SpareArray这样松散数组的支持。Parcel还可以包含Parcel子类型,于是进一步提升了其对对象封装处理的能力。最后,通过IBinder、Bundle、Serializable类型的支持几乎完善了Parcel所能传输对象的能力。Parcel所能完整支持数据类型有:
但Parcel并不提供完整的序列化支持,仅是一种跨进程传输时的高效手段,我们的对象在打包成Parcel时并非自动支持,都是靠writeToParcel()实现将对象分拆写入的,我们必须保证读与写的顺序一致,createFromParcel()与writeToParcel()里的操作必须是一一对应的。另外,Parcel对象被存储完成后,也是我们自定义的存储,如果有任何读写上的变动,则这一保存过的中间结果就失效了。如果有序列化的需求,必须通过Parcel来传递Bundle类型来实现。但从实现上来看,这样简化设计,也是我们得到了更高效地对象传输能力,在Parcel内部操作里,大部分还是靠JNI实现的更高效版本。所以,这种传输机制并不被称为序列化操作,而被称为Parcelable传输协议。
所以,在跨进程间的通信过程时,我们不但通过Binder来构造出了能够以RPC方式进行通信的能力,也得到了通过Parcel来传递复杂对象的能力。这样,我们的跨进程通信的能力就几乎可以完成任何操作了。在我们应用程序程序里,通过aidl设计,可以使构架更灵活,一些需要有后台长期存在又会被多个部分所共享的代码,都可以使用aidl实现,这时虽然会带来一定的进程调度上的开销,但使我们的构架将变得更加灵活。这样应用情境有下载、登录、第三方API、音乐播放等诸多使用上的实例。
Aidl对于Android系统的重要性更大,因为整个Android环境,都是构建在一种多进程的“沙盒”模型之上。为了支撑这种高灵活性的多进程设计,Android必然需要大量地使用aidl将系统功能拆分开。而aidl是仅对Java有效的一种编程环境,于是对于Android系统构架来说,aidl是Android系统实现里由Java构建的基础环境,我们每种需要暴露出来供应用程序使用的系统功能,必然都会是基于aidl实现的跨进程调用。当然,Android系统实现上并非全是由Java来构建的,对于底层一些有更高性能要求的代码,有可能也是由C/C++编写出来的,这就是我们在稍后要介绍的NativeService,但我们要知道,所谓的NativeService,也只不过是通过可执行代码接入到aidl环境,通过Native代码来模拟aidl的Service代码实现的。上层代码都使用跟aidl一模一样调用方式,底层实现的细节,对上层来说,是透明的。