Android IPC(一)Serializable、Parce
开启多进程
AndroidMenifest.xml
中给四大组件指定android:peocess
属性。
- 进程名以
:
开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中。 - 完整命名方式(全局进程),其他应用通过shareUID方式可以和它跑在同一进程中。
多进程产生的问题
- 静态成员和单例模式完全失效。
- 线程同步机制完全失效。
- SharePreferences的可靠性下降。
- Application会多次创建。
IPC基础
Serializable接口
Serializable
是Java所提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。
想让一个对象实现序列化,只需要实现Serializable
接口并指明一个serialVersionUID
即可,实际上,甚至这个serialVersionUID
也不是必须的,我们不需要声明这个UID同样也可以实现序列化,但是这将会对反序列化过程产生影响,具体什么影响后面在介绍。
public class User implements Serializable {
private static final long serialVersionUID = XXXXL;
....
}
通过Serializable方式来实现对象的序列化,实现起来非常简单,几乎所有的工作都被系统自动完成了。如何进行对象的序列化和反序列化也非常简单,只需要采用ObjectOutputStream
和ObjectInputStream
即可轻松实现。
//序列化过程
User user=new User(0,"amao",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(user);
in.close();
serialVersionUID的意义
UID的详细工作机制是这样的:序列化的时候系统会把当前类的UID写入序列化的文件中(也可能是其他中介),当反序列化的时候系统会去检测文件中的UID,看它是否和当前类的UID一致,如果一致就说明序列化的类的版本和当前类的版本是相同的,这个时候可以反序列化成功;否则就说明当前类和序列化的类相比发生了某些变化,比如成员变量的数量,类型可能发生改变,这个时候无法正常序列化,回报UID不同的错误。
一般来说,我们应该手动指定UID的值,这样在序列化和反序列化是两者的UID是相同,可以正常的进行反序列化。如果不手动指定UID,当你增加或删除某些成员变量的时候,那么系统会重新计算当前类的hash值并把它赋予给UID,这个时候当前的UID就和反序列化的UID就不同了,于是反序列化失败,程序crash掉了,但指定了就不会crash。当然,如果类的结构发生的改变,比如修改了类名,修改了成语变量的类型,这个时候尽管UID是一致的,反序列化还是会失败,因为类的结构有毁灭性的改变,根本无法从来版本的数据中还原出一个新的类结构对象。
最后,以几两点需要特别提一下
- 反序列化获取的对象与之前的对象数据完全一样,但是它们并不是同一个对象。
- 静态成员变量属于类不属于对象,所以不参加序列化过程
- 用transient关键字标记的成员变量不参与序列化过程。
Parcelable接口
Parcelable同样也是一个接口,通过Parcelable我们同样可以实现对象的序列化,而且远比Serializable高效,不够其过程就要复杂点了。
Parcelable
我们只要点击 Add Parcelable Implementation,AS会自动帮我们实现其方法,如下
public class Person implements Parcelable {
private int id;
private String name;
private boolean sex;
private Class cls;
protected Person(Parcel in) {
id = in.readInt();
name = in.readString();
sex = in.readByte() != 0;
cls = in.readParcelable(Class.class.getClassLoader());
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(id);
dest.writeString(name);
dest.writeByte((byte) (sex ? 1 : 0));
dest.writeParcelable(cls, flags);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<Person> CREATOR = new Creator<Person>() {
@Override
public Person createFromParcel(Parcel in) {
return new Person(in);
}
@Override
public Person[] newArray(int size) {
return new Person[size];
}
};
}
Parcel内部包装了可序列化的数据,可在Binder中传输,在上面代码中,我们可以看出,序列化过程需要实现序列化、反序列化和内容描述。
-
writeToParcel
完成序列化 ,通过Parcel的一系列write方法完成; -
CREATOR
完成反序列化,内部标明了如何创建序列化对象和数组,通过Parcel的一系列read方法来完成反序列过程; -
describeContents
完成内容描述,几乎所有的情况下都返回0,仅当当前对象中存在文件描述符时,该方法返回1。 -
在
Persion(Parcel in)
中,因为Class是另外一个可序列化的对象,所以它的反序列化过程需要传递当前线程的上下文加载器,否则会报无法找到类的错误。
其实Android系统有很多实现了Parcelable接口的类,如Intent、Bundle、Bitmap等等,我们可以直接序列化它们,如果List和Map中的元素都是可序列化的,那List和Map也是可以序列化的。
Serializable和Parcelable的区别
最大的区别就是存储媒介的不同:
Serializable使用IO读写存储在硬盘上;
Parcelable是直接在内存中读写,内存的读写速度远大于IO读写,所以Parcelable高效。
- 在使用内存时,Parcelable比Serializable性能高,推荐使用Parcelable;
- Serializable在序列化时会产生大量的临时变量,会引起频繁的GC;
- Parcelable不能使用在要将数据存储在磁盘上的情况,因为Parcleable不能很好的保证数据的持续性在外界有变化的情况下,尽管Serializable效率低点,也不提倡使用,但在这种情况下,还是建议使用Serializable。
Binder
Binder从不同角度上的定义:
- 直观来说,Binder是Android中的一个类,它实现了IBinder接口;
- 从IPC角度来说,Binder是Android中的一种跨进程通信方式,该通信方式在Linux中没有;
- 从Android Framework角度来说,Binder是ServiceManager连接各种Manager和相应ManagerService的桥梁;
- 从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据了。
Binder机制具体有两层含义:
- Binder是一种跨进程通信(IPC,Inter-Process Communication)的手段;
- Binder是一种远程过程调用(RPC,Remote Procedure Call)的手段。
Binder主要使用在Service中,包括AIDL和Messenger(Messenger的底层其实是AIDL)。
AIDL
AIDL:Android Interface Definition Language,安卓接口定义语言。
Android系统中的进程之间不能共享内存,因此,需要提供一些机制在不同进程之间进行数据通信。
通俗地讲,AIDL为我们提供一种快速实现Binder的工具。
AIDL文件代码如下:
// Book.aidl
package com.cooffee.studypro.aidl;
parcelable Book;
// IBookManager.aidl
package com.cooffee.studypro.aidl;
import com.cooffee.studypro.aidl.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
系统自动帮我们生成IBookManager接口的java代码文件(gen目录下),代码结构如下:
interface IBookManager extends IInterface {
abstract class Stub implements IBookManager {
asInterface() {...};
asBinder() {...};
onTransact() {...};
abstract class Proxy implements IBookManager {
asBinder() {...};
getInterfaceDescriptor() {...};
getBookList() {...};
addBook() {...};
}
}
getBookList(); // 抽象方法
addBook(); // 抽象方法
// 两个方法标记ID的声明...
}
IBookManager接口的核心是它的内部类Stub和Stub的内部代理类Proxy。
下面详细介绍针对这两个类的每个方法的定义:
-
DESCRIPTOR
Binder的唯一标识,一般用当前Binder的类名标识,比如“com.example.aidl.IBookManager”。 -
asInterface(android.os.IBinder obj)
将服务器端的Binder对象转换成客户端所需的AIDL接口类型的对象。
该转换过程是区分进程的:
如果客户端和服务端位于统一进程,此方法返回服务端的Stub对象本身;
否则返回的是系统封装的Stub.proxy对象。
-
asBinder()
用于返回当前Binder对象。 -
onTransact()
此方法运行在服务器端的Binder线程池,当客户端发起跨进程请求时,远程请求会通过系统低层封装后交由次方法来处理。 -
Proxy#getBookList:
此方法运行在客户端,当客户端远程调用此方法时,其内部实现如下:
- 创建该方法的输入型Parcel对象_data、输出型Parcel对象_reply和返回值对象List;
- 把该方法的参数信息写入_data(如果有参数);
- 调用transact方法来发起RPC(远程过程调用)请求,同时当前线程挂起;
- 服务器端的onTransact方法会被调用,直到RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果;
- 返回reply中的数据。
-
Proxy#addBook
执行过程与getBookList过程相同,但是该方法没有返回值。
说明一下:
-
当客户端发起远程请求时,由于当前线程会被挂起直至服务端进程返回数据,所以如果一个远程方法是耗时的,不能在UI线程发起此远程请求
-
由于服务端的Binder方法运行在Binder的线程池中,所以Binder方法不管是否耗时都应该采用同步的方式实现,因为它已经运行在一个线程中了。
Binder死亡代理
Binder运行在服务端进程中,如果服务端进程由于某种原因异常终止,这个时候我们到服务端的Binder连接断裂(称之为Binder死亡),会导致我们的远程调用失败。而且我们不知道Binder连接是什么时候断裂的,那么客户端的功能会受到影响。
Binder提供了两个配对的方法linkToDeath
和unlinkToDeath
,通过linkToDeath
我们可以为Binder设置一个死亡代理,当Binder死亡时,我们会收到通知,这个时候我们就可以重新发起连接请求从而恢复连接。
首先,声明一个DeathRecipient
对象。DeathRecipient
是一个接口,其内部只有一个方法binderDied
,我们需要实现这个方法,当Binder死亡的时候,系统就会回调binderDied
方法,然后我们就可以移出之前绑定的binder代理并重新绑定远程服务:
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
if (mBookManager == null)
return;
mBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
mBookManager = null;
// TODO: 重新绑定远程service
}
};
其次,在客户端绑定远程服务成功后,给binder设置死亡代理:
mService = IMessageBoxManager.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient, 0);
其中linkToDeath的第二个参数是个标记位,我们直接设为0即可。
另外,Binder的方法isBinderAlive也可以判断Binder是否死亡。