Service与Android系统设计(2)-- Parcel

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

共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. class TaskInfo {  
  2.     public long mPss;  
  3.     public long mTotalMemory;  
  4.     public long mElapsedTime;  
  5.     public int mPid;  
  6.     public int mUid;  
  7.     public String mPackageName;  
  8.    
  9.    TaskInfo() {  
  10.        mPss = 0;  
  11.        mTotalMemory = 0;  
  12.        mElapsedTime = 0;  
  13.        mPid = -1;  
  14.        mUid = -1;  
  15.        mPackageName = null;  
  16.     }  
  17. }  


我们通过这一个类来创建一个对象时,这个对象的有效使用范围只是在一个进程里,因为进程空间是各自独立的,在进程空间里,实际上这一对象是保存在堆空间(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. package org.lianlab.services;  
  2. import android.os.Parcel;  
  3. import android.os.Parcelable;  
  4.    
  5. public class TaskInfo implements Parcelable {  
  6.     public long mPss;  
  7.     public long mTotalMemory;  
  8.     public long mElapsedTime;  
  9.     public int mPid;  
  10.     public int mUid;  
  11.     public String mPackageName;  
  12.    
  13.    TaskInfo() {  
  14.        mPss = 0;  
  15.        mTotalMemory = 0;  
  16.        mElapsedTime = 0;  
  17.        mPid = -1;  
  18.        mUid = -1;  
  19.        mPackageName = null;  
  20.     }  
  21.    
  22.     public int describeContents() {                1  
  23.        return 0;  
  24.     }  
  25.    
  26.     public void writeToParcel(Parcel out, int flags) {    2  
  27.        out.writeLong(mPss);  
  28.        out.writeLong(mTotalMemory);  
  29.        out.writeLong(mElapsedTime);  
  30.        out.writeInt(mPid);  
  31.        out.writeInt(mUid);  
  32.        out.writeString(mPackageName);  
  33.     }  
  34.    
  35.     public static final Parcelable.Creator<TaskInfo>CREATOR = new Parcelable.Creator<TaskInfo>() {                 3  
  36.        public TaskInfo createFromParcel(Parcel in) {            4  
  37.            return new TaskInfo(in);  
  38.        }  
  39.    
  40.        public TaskInfo[] newArray(int size) {             5  
  41.            return new TaskInfo[size];  
  42.        }  
  43.     };  
  44.    
  45.     privateTaskInfo(Parcel in) {                 6  
  46.        mPss = in.readLong();  
  47.        mTotalMemory = in.readLong();  
  48.        mElapsedTime = in.readLong();  
  49.        mPid = in.readInt();  
  50.        mUid = in.readInt();  
  51.        mPackageName = in.readString();  
  52.     }  
  53. }  

  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. package org.lianlab.services;  
  2. importorg.lianlab.services.ITaskServiceCallback;  
  3. import org.lianlab.services.TaskInfo;  
  4.    
  5. interface ITaskService {  
  6.     intgetPid (ITaskServiceCallback callback);  
  7.    TaskInfo getTaskStatus ();  
  8. }  

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

[java]  view plain copy
  1. private final ITaskService.StubmTaskServiceBinder =new ITaskService.Stub() {  
  2.    public int getPid(ITaskServiceCallback callback) {  
  3.        mCount++;  
  4.        try {  
  5.             callback.valueCounted(mCount);  
  6.        } catch (RemoteException e) {  
  7.             e.printStackTrace();  
  8.        }  
  9.        return Process.myPid();  
  10.    }  
  11.   
  12.    public TaskInfo getTaskStatus() {  
  13.   
  14.        TaskInfo mTaskInfo = new TaskInfo();  
  15.        mTaskInfo.mUid = Process.myUid();  
  16.        Debug.MemoryInfo memInfo = new Debug.MemoryInfo();  
  17.        Debug.getMemoryInfo(memInfo);  
  18.        mTaskInfo.mPss = memInfo.nativePss;  
  19.   
  20.        Runtime runtime = Runtime.getRuntime();  
  21.        mTaskInfo.mTotalMemory = runtime.totalMemory();  
  22.   
  23.        mTaskInfo.mPid = Debug.getBinderReceivedTransactions();  
  24.        mTaskInfo.mElapsedTime = Process.getElapsedCpuTime();  
  25.        mTaskInfo.mPackageName = getPackageName();  
  26.        return mTaskInfo;  
  27.    }  
  28.   
  29. };  

在我们新增的getTaskStatus()方法里,我们会创建一个新的TaskInfo对象,然后根据当前的进程上下文环境给这一TaskInfo对象进行赋值,然后再返回这一对象。由于TaskInfo现在已经荣升成Parcelable对象了,于是这一返回,实际上会发生跨进程环境里,在Remote Service的调用端返回这一对象。

从上面的代码示例,我们大致也可以分析到通过Parcel来传递对象的原理。这跟我们中寄送由零部件组成的物品类似。生活中,我们寄运由零部件构成的物品,一般是把东西拆散成零组件,于是好包装也方便运输,把零部件尽可能灵活摆放塞进一个盒子里,再寄送出去。接收到这个包裹的那方,会从盒子里将零部件拆散开来,再按拆卸时同样的构架再将零部件组装到一起,于是我们就得到了原来的造型各式的物品。出于这样的类比性,于是我们的负责搬运的类叫Parcel,就是包裹的意思,而用于搬运的拆装过程,或者准确的是说是可拆卸然后再组装的能力,就叫称为Parcelable。

对于Parcel的传输,0xLab的黄敬群先生(JServ)作了一个形象的比喻,希望通过传真机来发送一个纸盒子到远端。传真机不是时空传送带,并不会真正可以实现某个物品的跨空间传递,但我们一定的变通来完成:

Service与Android系统设计(2)-- Parcel_第1张图片

       我们可以把一个纸盒子拆解摊开,这时得到一个完全平面化的带六个格子的一个图形。我们通过传真机传送时就有了可能,我们传真这个带六个格子的图形到远端,这时远端的传真机就会收到同是六个格子的传真图像,打印到一张纸上。然后我们再将这张纸裁减开来,重新粘贴到一起,于是也可以得到一个纸盒子,跟我们原始的纸盒有着一模一样的外观。

于是,在发送时,我们大体上可认为是把对象进行拆解打包,然后塞进Parcel对象里。Parcel此时相当于容器的作用,于是其内容一定会有一段buffer用于存放这种中间结果。于是这一过程就会是通过writeToParcel()方法,将对象的属性分拆开,填写到这个Parcel的buffer里:

Service与Android系统设计(2)-- Parcel_第2张图片

因为这一过程,在计算处理上相当于把原有来非线性存放的对象,通过平整化转移到一个线性的内存空间里进行保存,于是这一操作被称为平整化”flatter”。从前面的代码我们也可以看到,所谓的平整化操作,就是通过writeToParcel()写入到一个Parcel对象里。我们更细一点来观察的话,这一过程就是通过Parcel对象的writeInt()、writeLogn()、writeString()等不同操作方法,将对象的属性写入到Parcel的内置buffer里。

在读取的那一端,我们会通过某种手段,将Parcel对象读出来,通过CREATOR里定义的初始化方法,得到Parcel里保存的对象:

Service与Android系统设计(2)-- Parcel_第3张图片

对应于发送时的flatten,我们接收时的操作就是unflatten,将对象从一个线性化保存在Parcel的内置buffer里的数据,还原成具体的对象。这一步骤会是通过CREATOR里通过Parcel来初始化对象的createFromParcel()来完成。

到此,我们至少就得到通过一个中间区域来搬运与传送对象的能力。对象不能直接拷贝与传输,但线性化内存是可以的,我们可以把内存从0到结果的位置通过某种IPC机制发送出去,在另一端进行接收,就可以得到这段内存的内容。最后,如果我们把Parcel对象的这段内容通过Binder通信来进行传输,此时我们就得到了进程间对象互相传送的能力:

Service与Android系统设计(2)-- Parcel_第4张图片

我们在进行跨进程的对象传递过程里,都通过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一模一样调用方式,底层实现的细节,对上层来说,是透明的。

你可能感兴趣的:(Service与Android系统设计(2)-- Parcel)