IPC简介
IPC的含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程
多进程的情况分为两种:
1. 一个应用因为某些原因自身需要采用多进程模式来实现
2. 当前应用需要向其他应用获取数据
Android中的多进程模式
开启多进程模式
正常情况下,在Android中多进程是指一个应用中存在多个进程的情况。
在Android中使用多进程的方法:
- 给四大组件在AndroidMenifest中指定android:process属性(我们无法给一个线程或一个实体类指定其运行时所在的进程)
- 非常规方法:通过JNI在native层去fork一个新的进程
暂时先看第一种,AndroidMenifest中没有给某个组件指定process属性时默认运行在默认进程中,默认进程的名字为程序包名。如果要指定,有两种方式。
android:process=":remote"
android:process="com.ryg.chapter_2.remote"
两种方式的区别:
- 第一种 ":"的含义是在当前进程名前附加上当前的包名,这是一种简写的方法,完整进程名为:com.ryg.chapter_2:remote
- 第二种是一种完整的命名方式,不会附加包名信息
- 第一种属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中
- 第二种属于全局进程,其他应用可以通过ShareUID的方式和它跑在同一个进程中
注:Android系统会为每一个应用分配一个唯一的UID,具有相同UID的应用才能共享数据。
两个应用通过ShareUID跑在同一个进程中是有要求的,需要这两个应用具有相同的ShareUID并且签名相同才可以(它们看起来就像一个应用的两个部分)
具有相同ShareUID并且签名相同的两个应用,可以共享对方私有数据,比如data目录、组件信息等。
如果ShareUID相同、签名相同且跑在同一进程中,那么除了能共享data目录、组件信息等,还能共享内存数据。
多进程模式的运行机制
我们新建一个类UserManager,类中有一个public的静态成员变量(静态变量是可以在所有的地方共享的,并且一处有修改处处都会同步)
public class UserManager {
public static int sUserId = 1;
}
在MainActivity的onCreate中我们把这个sUserId重新赋值为2,打印这个静态变量值后再启动SecondActivity,在SecondActivity中再打印一下sUserId的值。运行结果却是SecondActivity中打印的sUserId的值还是1。
问题出现的原因
Android为每一个应用(或每个进程)分配了一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,导致在不同的虚拟机中访问同一个类的对象会产生多份副本。 拿本例来说,在进程com.ryg.chapter_2和com.ryg.chapter_2:remote中都存在一个UserManager类,这两个类是互不干扰的,在一个进程中修改sUserId的值只会影响当前进程,对其他进程不会造成任何影响。
多进程带来的主要影响:所有运行在不同进程中的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败(正常情况下通过一些中间层来共享数据)
多进程会造成的问题
- 静态成员和单例模式完全失效
- 线程同步机制完全失效(不管是锁对象还是锁全局类都无法保证线程同步,因为不同进程锁的不是同一个对象)
- SharedPreferences的可靠性下降(不支持两个进程同时执行写操作,否则会导致一定几率的数据丢失。底层是通过读/写XML文件实现,并发写可能会出问题)
- Application会多次创建
当一个组件跑在一个新的进程中的时候,由于系统要在创建新的进程同时分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程。相当于系统把这个应用重新启动了一遍,运行在不同进程中的组件是属于两个不同的虚拟机和Application的。如果连续启动三个同一个应用内但属于不同进程的Activity,Application的onCreate应该执行三次并打印出三次进程名不同的log。这就证实了不同进程的组件的确会拥有独立的虚拟机、Application以及内存空间,它就相当于两个不同的应用采用了SharedUID的模式。
IPC基础概念
主要包含三方面内容:Serializable接口、Parcelable接口以及Binder。当我们需要通过Intent和Binder传输数据时就需要使用Parcelable或者Serializable。还有的时候我们需要把对象持久化到存储设备上或者通过网络传输(以字节流的方式对数据进行传输)给其他客户端,也需要Serializable来完成对象的持久化。
序列化
我们传输的数据如果是基本数据类型直接进行相关传递即可,但是数据类型比较复杂的时候就需要进行序列化。
它的目的:
- 永久地保存对象数据,保存对象的字节序列到本地文件中
- 通过序列化对象在网络中传递对象
- 通过序列化在进程间传递对象
Serializable接口
java中的序列化接口
使用Serializable实现序列化:
- 这个类实现Serializable接口
- 声明一个serialVersionUID(不是必需的,不声明会对反序列化过程产生影响)
private static final long serialVersionUID = 519067123721295773L;
//序列化过程
User user = new User(0, "jake", true);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
out.writeObject(user);
out.close();
//反序列化过程
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt"));
User newUser = (User) in.readObject();
in.close();
serialVersionUID
工作机制
序列化的时候系统把当前类的serialVersionUID写入序列化的文件中(也有可能是其他中介),当反序列化的时候就回去检测文件中的serialVersionUID,看它是否和当前类的serialVersionUID一致,如果一致就说明序列化的类的版本和当前类的版本是相同的,这时候就可以成功反序列化;否则说明当前类和序列化的类相比发生了某些变换,比如成员变量的数量、类型可能发生了改变,这个时候是无法正常反序列化的(报java.io.InvalidClassException)。
赋值的两种方法
一般来说,应该手动指定serialVersionUID的值,比如1L,也可以让系统根据当前类的结构自动生成它的hash值(本质一样)。
如果不手动指定serialVersionUID的值,反序列化时当前类有所改变,比如增加或删除了某些成员变量,系统会重新计算当前类的hash值并把它赋值给serialVersionUID,这时候类的serialVersionUID就和序列化数据中的serialVersionUID不一致,于是反序列化失败,程序就会出现crash,所以手动指定serialVersionUID可以很大程度避免反序列化过程的失败。如果类结构有了毁灭性的改变(修改类名,修改成员变量的类型等),尽管serialVersionUID验证通过,反序列化过程还是会失败。
注:静态成员变量属于类不属于对象,不会参与序列化过程;用transient关键字标记的成员变量不参与序列化过程
Parcelable接口
Android中的序列化方式
//不仅仅需要声明,还需要实现内部的相应方法
public class User implements Parcelable {
public int userId;
public String userName;
//另一个可序列化对象
public Book book;
public User(int userId, String userName) {
this.userId = userId;
this.userName = userName;
}
/**
* 内容描述功能
* 仅当当前对象中存在文件描述符时,此方法返回1
*/
@Override
public int describeContents() {
return 0;
}
/**
* 将当前对象写入序列化结构中
* @param out
* @param flags 为1时标识当前对象需要作为返回值返回,不能立即释放资源,几乎所有情况都为0
*/
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(userId);
out.writeString(userName);
//writeParcelable()里又调用了 writeToParcel()
out.writeParcelable(book, 0);
}
//反序列化
public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
/**
* 从序列化后的对象中创建原始对象
* 其中public static final一个都不能少,内部对象CREATOR的名称也不能改变,必须全部大写。
* @param in
* @return
*/
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
/**
* 创建原始对象数组
* @param size
* @return
*/
@Override
public User[] newArray(int size) {
return new User[size];
}
};
private User(Parcel in) {
//如果元素数据类型是list时, list = new ArrayList<?>(); in.readList(list, getClass().getClassLoader();
userId = in.readInt();
userName = in.readString();
//反序列化过程需要传递当前线程的上下文类加载器,否则会报无法找到类的错误
book = in.readParcelable(Thread.currentThread().getContextClassLoader());
}
}
注:写入数据的顺序和读出数据的顺序必须是相同的.
未提供writeBoolean方法 1为true 0为false
可以直接序列化(实现了Parcelable接口的类)的比如Intent、Bundle、Bitmap等,同时List和Map也可序列化,前提是它们里面的每个元素都是可序列化的。
Serializable和Parcelable比较
Serializable | Parcelable |
---|---|
java的序列化的接口,使用简单 | Android中的序列化方式,使用麻烦 |
开销大,序列化和反序列话的时候需要大量的I/O操作 | 基于内存操作,效率高 |
Parcelable主要用于内存序列化。Serializable主要用于序列化到存储设备或网络传输。