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

Parcel与Parcelable

当我们在调用远程方法时,需要在进程间传递参数以及返回结果。这种类似的处理方式,需要把数据与进程相关性去除,变成一种中间形式,然后按统一的接口进行读写操作。这样的机制,一般在高级编程语言里都被称为序列化。 在Android世界里处理数据的序列化操作的,使用了一种Parcel类,而能够处理数据序列能力,则是实现Parcelable接口来实现。于是,当我们需要在进程间传输一个对象,则实现这一对象的类必须实现Parcelable接口里定义的相应属性或方法,而在使用这一对象时,则可以使用一个Parcel引用来处理传输时的基本操作。 前面说明的AIDL编程,都只是针对String,Int等Java的基本数据类型,如果我们需要处理一些复杂的数据类型,或是需要定义一些自定义的数据类型,这时,我们需要aidl里的另一种效用,导入Parcelable接口。

在aidl环境里使用Parcelable接口,相对来说会更简单。我们前面也强调过,在aidl工作的实现上,为了达到简洁高效的设计目标,aidl只支持基本的数据类型,一个接口类在aidl环境里仅会定义方法,而不会涉及属性定义,所以这种限制作用到Parcelable,Parcelable在aidl会只是简单的一行,其他aidl使用到这些Parcelable接口的部分,可以直接引用定义这一Parcelable的aidl文件即可。

基于我们前面的例子来拓展一个Parcelable的例子,假如我们希望在进程能够传递一些进程间的描述信息,我们会把这样的数据组织到一个类时进行传输,我们可以把这个类叫TaskInfo,于是在单进程环境里,我们大致会有这样一个类:

[java] view plain copy
  1. classTaskInfo{
  2. publiclongmPss;
  3. publiclongmTotalMemory;
  4. publiclongmElapsedTime;
  5. publicintmPid;
  6. publicintmUid;
  7. publicStringmPackageName;
  8. TaskInfo(){
  9. mPss=0;
  10. mTotalMemory=0;
  11. mElapsedTime=0;
  12. mPid=-1;
  13. mUid=-1;
  14. mPackageName=null;
  15. }
  16. }


我们通过这一个类来创建一个对象时,这个对象的有效使用范围只是在一个进程里,因为进程空间是各自独立的,在进程空间里,实际上这一对象是保存在堆空间(Heap)里的。如果我们希望通过IPC机制来直接传递这么一个对象也是不现实的,因为在Java环境里对象是通过引用来访问,一个对象里对其他对象,比如访问TaskInfo类的packageName,则是会通过一个类似于指针的引用来访问。

所以,通过IPC来复制结构比较复杂的对象时,必须要通过某种机制可以将对象拆解成一种中间格式,通过在IPC里传输这种中间格式然后得到对象在进程之间互相传递的效果。无论是使用什么样的IPC机制,这种传输过程都是会是一边把数据写进去,另一边读出来,根据我们前面的分析,我们使用一个继承自Binder的对象即可完成这种功能。但这种读写两端的代码会不停重复,也容易引起潜在的错误,Android系统会使用面向对象的技巧来简化这种操作。与进程间方法的相互调用不同,在进程间传递对象不在乎交互,而更关注使用时的灵活性。在对象传递时,共性是传输机制,差异性是传输的对象构成,于是我们在Binder类之上,再派生出一个Parcel类,专用于处理属于共性部分的数据传递,而针对差异性部分对象的构成,则通过一个Parcelable的接口类,可以通过这一接口将对象组成上的差异性通知到Parcel。

有了Parcel类与Parcelable接口类之后,我们要传递像TaskInfo对象则变得更容易。我们可以让我们的TaskInfo类来实现Parcelable功能,使这一类生成的对象,都将在底层通过Parcel对象来完成底层的传输。我们前面定义的TaskInfo类,则会被变成如下的样子:

[java] view plain copy
  1. packageorg.lianlab.services;
  2. importandroid.os.Parcel;
  3. importandroid.os.Parcelable;
  4. publicclassTaskInfoimplementsParcelable{
  5. publiclongmPss;
  6. publiclongmTotalMemory;
  7. publiclongmElapsedTime;
  8. publicintmPid;
  9. publicintmUid;
  10. publicStringmPackageName;
  11. TaskInfo(){
  12. mPss=0;
  13. mTotalMemory=0;
  14. mElapsedTime=0;
  15. mPid=-1;
  16. mUid=-1;
  17. mPackageName=null;
  18. }
  19. publicintdescribeContents(){1
  20. return0;
  21. }
  22. publicvoidwriteToParcel(Parcelout,intflags){2
  23. out.writeLong(mPss);
  24. out.writeLong(mTotalMemory);
  25. out.writeLong(mElapsedTime);
  26. out.writeInt(mPid);
  27. out.writeInt(mUid);
  28. out.writeString(mPackageName);
  29. }
  30. publicstaticfinalParcelable.Creator<TaskInfo>CREATOR=newParcelable.Creator<TaskInfo>(){3
  31. publicTaskInfocreateFromParcel(Parcelin){4
  32. returnnewTaskInfo(in);
  33. }
  34. publicTaskInfo[]newArray(intsize){5
  35. returnnewTaskInfo[size];
  36. }
  37. };
  38. privateTaskInfo(Parcelin){6
  39. mPss=in.readLong();
  40. mTotalMemory=in.readLong();
  41. mElapsedTime=in.readLong();
  42. mPid=in.readInt();
  43. mUid=in.readInt();
  44. mPackageName=in.readString();
  45. }
  46. }

  1. describeContents(),Parcelabl所需要的接口方法之一,必须实现。这一方法作用很简单,就是通过返回的整形来描述这一Parcel是起什么作用的,通过这一整形每个bit来描述其类型,一般会返回0。
  2. writeToParcel(),Parcelabl所需要的接口方法之二,必须实现。writeToParcel()方法的作用是发送,就是将类所需要传输的属性写到Parcel里,被用来提供发送功能的Parcel,会作为第一个参数传入,于是在这个方法里都是使用writeInt()、writeLong()写入到Parcel里。这一方法的第二参数是一个flag值,可以用来指定这样的发送是单向还是双向的,可以与aidl的in、out、inout三种限定符匹配。
  3. CREATOR对象,Parcelable接口所需要的第三项,必须提供实现,但这是一个是接口对象。正如我们看到的,这一CREATOR对象,是使用模板类Parcelable.Creator,套用到TaskInfo来得到的,Parcelable.Creator<TaskInfo>。这个CREATOR对象在很大程度上是一个工厂(Factory)类,用于远程对象在接收端的创建。从某种意义上来说,writeToParcel()与CREATOR是一一对应的,发送端进程通过writeToParcel(),使用一个Parcel对象将中间结果保存起来,而接收端进程则会使用CREATOR对象把作为Parcel对象的中间对象再恢复出来,通过类的初始化方法以这个Parcel对象为基础来创建新对象。后续的4-6,则是完成这个CREATOR对象的实现。
  4. createFromParcel(),这是ParcelableCreator<T>模板类所必须实现的接口方法,提供从Parcel转义出新的对象的能力。接收端来接收传输过来的Parcel对象时,便会以这一个接口方法来取得对象。我们这里直接调用基于Parcel 的类的初始化方法,然后将创建的对象返回。
  5. newArray(),这是ParcelableCreator<T>模板类所必须实现的另一个接口方法,但这一方法用于创建多个这种实现了Parcelable接口的类。通过这一方法,CREATOR对象不光能创建单个对象,也能返回多个创建好的空对象,但多个对象不能以某个Parcel对象为基础创建,于是会使用默认的类创始化方法。
  6. 实现具体的以Parcel为参照的初始化方法,这并非必须,我们也可以在createFromParcel()里直接根据Parcel的值赋值到对象来实现,但这样实现则更清晰。这一方法,基本上与writeToParcel()是成对的,以什么顺序将对象属性写入Parcel,则在createFromParcel()会就会以同样的顺序对象属性从Parcel里读出来,使用Parcel的readInt()、readLong()等方法来完成。

通过上述的这一个类的定义,我们便得到了一个可以被跨进程传输的类,通过这个类所创建的对象,可以无缝地在进程进行传输。如果需要使用这一我们已经定义好的 Parcelable类,我们只需要新建一个aidl文件,描述这一Parcelable的存在信息,从而通知到aidl编译工具。如果是基于我们前面例子里定义的Parcelable类TaskInfo,我们会需要使用一个与之同名的,叫TaskInfo.aidl的文件:

package org.lianlab.services;

parcelableTaskInfo;

这一文件很简单,实际就是一行用于定义包名,一行定义存在一个实现了Parcelable接口的TaskInfo类。然后,我们在需要使用它的部分再将这一aidl文件导入,我们可以在前面的ITaskService.aidl进行拓展,定义一个新的getTaskStatus()接口方法来返回一个TaskInfo对象。在这些定义接口方法的aidl文件里使用Parcelable类很简单,只需要引用定义这一Parcelable类的aidl文件即可:

[java] view plain copy
  1. packageorg.lianlab.services;
  2. importorg.lianlab.services.ITaskServiceCallback;
  3. importorg.lianlab.services.TaskInfo;
  4. interfaceITaskService{
  5. intgetPid(ITaskServiceCallbackcallback);
  6. TaskInfogetTaskStatus();
  7. }

当然,这时我们的ITaskService接口变掉了,于是我们会需要改写我们的Stub类的实现,我们需要到TaskService.java的实现里,新增强getTaskStatus()方法的实现:

[java] view plain copy
  1. privatefinalITaskService.StubmTaskServiceBinder=newITaskService.Stub(){
  2. publicintgetPid(ITaskServiceCallbackcallback){
  3. mCount++;
  4. try{
  5. callback.valueCounted(mCount);
  6. }catch(RemoteExceptione){
  7. e.printStackTrace();
  8. }
  9. returnProcess.myPid();
  10. }
  11. publicTaskInfogetTaskStatus(){
  12. TaskInfomTaskInfo=newTaskInfo();
  13. mTaskInfo.mUid=Process.myUid();
  14. Debug.MemoryInfomemInfo=newDebug.MemoryInfo();
  15. Debug.getMemoryInfo(memInfo);
  16. mTaskInfo.mPss=memInfo.nativePss;
  17. Runtimeruntime=Runtime.getRuntime();
  18. mTaskInfo.mTotalMemory=runtime.totalMemory();
  19. mTaskInfo.mPid=Debug.getBinderReceivedTransactions();
  20. mTaskInfo.mElapsedTime=Process.getElapsedCpuTime();
  21. mTaskInfo.mPackageName=getPackageName();
  22. returnmTaskInfo;
  23. }
  24. };

在我们新增的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所能完整支持数据类型有:

  • null
  • String
  • Byte
  • Short
  • Integer
  • Long
  • Float
  • Double
  • Boolean
  • String[]
  • boolean[]
  • byte[]
  • int[]
  • long[]
  • Object[]
  • Bundle
  • Map
  • Parcelable
  • Parcelable[]
  • CharSequence
  • List
  • SparseArray
  • IBinder
  • Serializable

但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一模一样调用方式,底层实现的细节,对上层来说,是透明的。

更多相关文章

  1. Android学习笔记之ImageSwitcher
  2. Android(安卓)Snackbar使用详解
  3. Android可绘制对象资源之shape和layer-list使用
  4. 静态扫描工具提升Android代码质量
  5. Android(安卓)SAX方式解析XML文件
  6. Android坐标的简单方法介绍
  7. Android(安卓)对象型数据库 db4o
  8. Android(安卓)游戏开发之服务器端
  9. J2ME移植Android

随机推荐

  1. Android下使用JDOM访问XML文件
  2. 解决Error running app:Instant Run requ
  3. [原]Ubuntu 14.04编译Android Kernel
  4. Android学习能力之统计日志上传设计
  5. android 项目中接入flutter模块(部分页面
  6. Android–手掌中的王者?
  7. Android优秀开源项目(一)
  8. Android AOSP源码下载编译
  9. Android编译错误:You have tried to chang
  10. android 开发中中,经常用到的代码