《Android开发艺术探讨》之 Android IPC 介绍
IPC是 Inter-Proscess Communication的缩写,含义为进程间的通讯或者跨进程通讯,是指两个进程之间进行数据交换的过程。按操作系统的中的描述,线程是CPU调度最小的单元,同时线程是一种有限的系统资源,而进程是指一个执行单元,在PC和移动设备上指一个程序或者一个应用。一个进程可以包含多个线程,因此进程和线程是包含于被包含的关系。
IPC的使用场景就必须提到多进程,只有面对多进程这种场景下,才需要考虑进程间通讯。多进程的情况分为两种:第一种是一个应用因为某些原因自身需要采用多进程模式来实现,原因有很多,应用特殊原因需要运行的单独的进程中,或者为了加大一个应用可使用内存所以需要通过多进程来获取多分内存空间。另外一种情况是:当前应用需要向其他应用获取数据,由于是两个应用,所以必须采取跨进程方式来获取所需要数据。
Android中的多进程模式
开启Android多进程模式很简单,就是给四大组件(Activity,Service,Receiver,ContentProvider)在AndroidMenifest中指定android:process属性。另外还有一种非常规的做法,那就是通过JNI在native层去fork一个新的进程。
给process指定多进程有两种不同的形式
- :remote
进程名以 “:”的含义是指要在进程名前面附加上当前的包名,这个进程属于当前应用的私有进程,其他应用不可以和他跑在同一个进程。 - com.xxx.xxx
这种属于全局进程,其他应用可以通过ShareUID方式可以和它跑在同一个进程,我们都知道系统会为每个应用分配一个唯一的UID,具有相同UID的应用才能共享数据。两个应用通过ShareUID跑在同一个进程,是需要相同的ShareUID并且签名相同才可以。不管它们是不是跑在同一个进程中,具有相同ShareUID的它们可以访问对方的私有数据,如:data目录、组件信息等。当然如果是在同一个进程中,除了data目录、组件信息还能共享内存数据。
多进程运行机制
我们知道Android为每一个应用分配了一个独立的虚拟机,或者说为每一个进程都分配了一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同的虚拟机访问同一个类的对象会产生多分副本。
所有运行在不同进程中的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败,这也是多进程所带来的主要影响,一般来说,使用多进程会造成如下几方面的问题。
- 静态成员和单例模式完全失效(都是不同的虚拟机)
- 线程同步机制完全失效(都不是同一块内存了)
- SharePreferences 的可靠性下降(底层通过XML去执行操作的,并发很可能出问题,甚至并发读、写都有可能出问题)
- Application会多次创建(当一个组件跑在一个新的进程中,由于系统要在创建新的进程同时分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程,因此系统又把这个应用重新启动了一遍,既然都重新启动了,那么自然会创建新的Application)
IPC基础概念介绍
- Serializable接口
Serializable是Java所提供的一个序列号接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。使用Serializable相当简单,只需要实现Serializable接口并声明一个serialVersionUID,其实这个serialVersionUID也不是必需的,如果不声明这个serialVersionUID也是可以实现序列化的,但是这将会对反序列化过程产生影响。
//序列化
User user = new User("xia","123455");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
out.write(user);
out.close();
//反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt"));
User newUser = (User)in.readObject();
in.close();
serialVersionUID是用来辅助序列化和反序列化过程的,原则上序列化后的数据中serialVersionUID只有和当前类serialVersionUID相同才能够正常的被反序列化。serialVersionUID的详细工作机制是这样的:序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中(也可能是其他中介),但反序列化的时候会去检测文件中的serialVersionUID,看它是否和当前类的serialVersionUID一致,如果一致就说明序列化的版本和当前版本是相同的,这个时候可以成功的反序列化,否则就说明当前类和序列化的类相比发生了某些变换,比如成员变量的数量、类型发生了改变,这个时候无法正常反序列化。
一般来说,我们应该手动指定serialVersionUID的值,如1L,也可以根据自身结构自动去生成它的hash值,这样序列化和反序列化时两者的serialVersionUID是相同的。如果不指定serialVersionUID的值,反序列化时当前类有所改变,比如增加或者删除了某些成员变量,那么系统就会重新计算当前类型的hash值并把它赋值给serialVersionUID,这个时候当前类的serialVersionUID就和序列化数据中的serialVersionUID不一致,于是反序列化失败,程序就会出现crash。所以避免反序列化过程的失败。比如当版本升级后,我们很可能删除了某个成员变量也可能增加了一些新的成员变量,这个时候序列化过程仍然能够成功,程序可以最大限度地恢复数据,相反,如果不指定serialVersionUID的话,程序则会挂掉。当然我们还要考虑另外一种情况,如果类的结构发生了非常规性的改变,比如修改了类名,修改了成员变量的类型,这个时候尽管serialVersionUID验证通过,但是反序列化还是会失败,因为类结构有了毁灭性的改变,根本无法从老版本的数据中还原出一个新的类结构对象。
静态成员变量属于类不属于对象,所以不会参与序列化过程,其次用transient关键字标记的成员变量不参与序列化配置。
- Parceable 接口
Parceable也是一个接口,只有实现这个接口,一个类的对象就可以实现序列化并可以通过Intent和Binder传递。
public class User implements Parcelable {
public int UserId;
public String userName;
public boolean isMale;
protected User(Parcel in) {
//从序列化后的对象中创建原始对象
UserId = in.readInt();
userName = in.readString();
isMale = in.readByte() != 0;
}
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
//从序列化后的对象中创建原始对象
return new User(in);
}
@Override
public User[] newArray(int size) {
//创建指定长度的原始对象数组
return new User[size];
}
};
@Override
public int describeContents() {
/**
返回当前对象的内容描述。如果含有文件描述符,返回1,否则返回0,几乎所有情况都返回0
*/
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
/**将当前对象写入序列化结构中,其中flags,标识有0或1
为1时标识当前对象需要作返回值返回,不能立即释放资源,几乎所有情况 都为0**/
dest.writeInt(UserId);
dest.writeString(userName);
dest.writeByte((byte) (isMale ? 1 : 0));
}
}
Parcel内部包装了可序列化的数据,可以在Binder中自由传输,从上述代码中可以看出,在序列化过程中需要实现的功能有序列化、反序列化和内部描述序列化功能由writeParcel方法完成,最终是通过Parcel中的一系列write方法来完成的。反序列化功能由CREATOR来完成,其内部标明了如何创建序列化对象和数组,并通过Parcel一系列read方法来完成反序列化过程;内容描述功能由describeContents来完成,几乎所有情况下这个方法都应该返回0,仅当当前对象中存在文件描述符时,此方法返回1.系统已经提供了许多实现Parcelable接口的类,它们都是可以直接序列化的,如:Intent、Bundle、Bitmap等,同时List 和 Map也可以序列化,前提时它们里面每个元素都是可序列化的。
既然Parcelable 和Serializable 都可以用于Intent间的数据传递,那么如何选择了。
- Serializable是Java中的序列化接口,其使用起来简单但是开销大,序列化和反序列化过程都需要大量的 I/O操作。
- Parcelable是Android中的序列化方式,更适用于在Android平台上,它的缺点就是用起来稍微麻烦,但效率很高,这是Android推荐方式,因此,首选Parcelable。但通过Parcelable将对象序列化到存储设备中或将对象序列化后通过网络传输也都是可以的,但是这个过程会稍显复杂,因此这种情况下建议使用Serializable。
- Binder
Binder是一个非常复杂,这里只是介绍下Binder的使用及上层实现原理。
Binder是Android中的一个类,它实现了IBinder的接口。从IPC角度来说,Binder是Android中一种跨进程的通讯方式,Binder还可以理解为一种虚拟物理设备,它的设备驱动是 /dev/binder,该通讯方式在Linux中没有;从Android Framework,角度来说,Binder是ServiceManger连接各种Manger(ActivityManger 、WindowManger,等等)和相应的MangeSrervice的桥梁;从Android应用层来说,
Binder是客户端和服务端进行通讯的媒介,当bindSrervice的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以用获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。普通Srervice中的Binder不涉及进程间通信,下面通过AIDL来分析Binder的工作过程。
//Book.java
public class Book i