2.1 Android IPC简介
- Linux的IPC通信机制:
命名通道、共享内存、信号量等来进行IPC。 - Android系统使用Binder机制来实现IPC,也可以使用Socket实现任意两个终端之间的通信。
2.2 Android中的多进程模式
-
通过给四大组件指定android:process属性就可以开启多进程模式。
- 默认进程的进程名是包名packageName
- 进程名以:开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,
- 进程名不以:开头的进程属于全局进程,其他应用通过ShareUID方法可以和它跑在同一个进程中。
- 两个应用必须有相同的ShareUID而且签名相同,才可以泡在同一个进程。
android系统会为每个进程分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,所以不同的虚拟机中访问同一个类的对象会产生多个副本。
-
多进程会造成几个问题:
- 静态成员和单例模式失效
- 线程同步机制失效
- sharedPreferences失效(sharedPreferences不支持两个进程同时去执行写操作)
- Application会多次创建。(系统要在创建新的进程的同时分配独立的虚拟机,应用会重新启动一次,也就会创建新的Application。)
2.3 IPC基础概念
2.3.1 Serializable接口
- serialVersionUId是一串long型数字,主要是用来辅助序列化和反序列化的,原则上序列化后的数据中的serialVersionUId只有和当前类的serialVersionUId相同才能够正常地被反序列化。
- 静态成员变量属于类不属于对象,因此不会参与序列化过程
- transient关键字标记的成员变量不参与序列化过程
2.3.2 Parcelable接口
Parcelable接口内部包装了可序列化的数据,可以在Binder中自由传输,Parcelable主要用在内存序列化上,可以直接序列化的有Intent、Bundle、Bitmap以及List和Map等等
public class Book implements Parcelable {
public int bookId;
public String bookName;
public Book() {
}
public Book(int bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName;
}
//“内容描述”,如果含有文件描述符返回1,否则返回0,几乎所有情况下都是返回0
public int describeContents() {
return 0;
}
//实现序列化操作,flags标识只有0和1,1表示标识当前对象需要作为返回值返回,不能立即释放资源,几乎所有情况都为0
public void writeToParcel(Parcel out, int flags) {
out.writeInt(bookId);
out.writeString(bookName);
}
//实现反序列化操作
public static final Parcelable.Creator<Book> CREATOR = new Parcelable.Creator<Book>() {
//从序列化后的对象中创建原始对象
public Book createFromParcel(Parcel in) {
return new Book(in);
}
public Book[] newArray(int size) {//创建指定长度的原始对象数组
return new Book[size];
}
};
private Book(Parcel in) {
bookId = in.readInt();
bookName = in.readString();
}
}
注意:
如果有一个实现了Parcelable接口的User类,Book对象是User对象的成员。那么在User的反序列化时,需要传递当前线程的上下类加载器
private User(Parcel in){
book = in.readParcelable(Thread.currentThread().getContextClassloader());
}
2.3.3 Binder接口
- 概念:
- Binder是Android中的一个类,它实现了IBinder接口。
- 从IPC角度看,Binder是Android中一种跨进程通信的方式;
- Binder还可以理解为虚拟的物理设备,它的设备驱动是/dev/binder;
- 从Framework层角度看,Binder是ServiceManager连接各种Manager和相应的ManagerService的桥梁;
- 从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据。
- aidl生成的java接口解析
- 声明了几个接口方法
- 声明一个内部类Stub。Stub是一个Binder类。当客户端和服务端在一个进程内时,方法调用不会走跨进程的transact;当客户端和服务端处于不同进程,方法调用要走跨进程的transact,这个逻辑由Stub的内部代理类Proxy来完成。
- Stub类的方法定义:
- asInterface(android.os.IBinder obj):将服务端的Binder对象转换成客户端所需的AIDL接口类型的对象,这种转换过程是区分进程的,如果客户端和服务端是在同一个进程中,那么这个方法返回的是服务端的Stub对象本身,否则返回的是系统封装的Stub.Proxy对象。
- asBinder:返回当前Binder对象。
- onTransact:这个方法运行在服务端中的Binder线程池中,当客户端发起跨进程请求(mRemote.transact)时,远程请求会通过系统底层封装后交由此方法来处理。
- 声明了几个标识接口方法的整型id(static)。这几个id用于标识在transact过程中客户端所请求的到底是哪个方法。
- 声明DESCRIPTIOR: Binder唯一标识,一般用类名表示
- Proxy的方法定义:
- asBinder:返回服务端的Binder对象
- getInterfaceDescriptor: 返回DESCRIPTIOR
- Proxy#XXX接口方法:所有的接口方法都是调用mRemote.transact(id, data_parcel, reply_parcel, 0)。流程为:客户端发送RPC调用请求,同时客户端当前线程挂起;然后服务端onTransact方法调用,直到RPC过程返回后,当前客户端线程继续执行,从reply_parcel中取出RPC过程中的返回结果。
- linkToDeath/unlinkToDeath
Binder运行在服务端,如果由于某种原因服务端异常终止了的话会导致客户端的远程调用失败,所以Binder提供了两个配对的方法linkToDeath和unlinkToDeath。
如何给Binder设置死亡代理:
1. 声明一个DeathRecipient对象,DeathRecipient是一个接口,其内部只有一个方法bindeDied,实现这个方法在Binder死亡的时候收到。
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
if (mRemoteBookManager == null) return;
mRemoteBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
mRemoteBookManager = null;
// TODO:这里重新绑定远程Service
}
};
2. 在客户端绑定远程服务成功之后,给binder设置死亡代理
mRemoteBookManager.asBinder().linkToDeath(mDeathRecipient, 0);
2.4 Android中的IPC方式
2.4.1. 使用Bundle:
Bundle实现了Parcelable接口,Activity、Service和Receiver都支持在Intent中传递Bundle数据。
2.4.2. 使用文件共享:
这种方式简单,适合在对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读写的问题。 SharedPreferences是一个特例,虽然它也是文件的一种,但是由于系统对它的读写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读写就变得不可靠,当面对高并发读写访问的时候,有很大几率会丢失数据,因此,不建议在进程间通信中使用SharedPreferences。
2.4.3. 使用Messenger:Messenger是一种轻量级的IPC方案,它的底层实现就是AIDL。Messenger是以串行的方式处理请求的,即服务端只能一个个处理,不存在并发执行的情形。
Messenger中进行数据传输必须将数据放入Message中,一般情况下
- 服务端会建一个Hander和Messenger对应,并通过mMessenger.getBinder()暴露Binder接口给客户端
- 客户端通过new Messenger(binder)得到Messenger,然后通过mMessenger.send(mMessage)来实现数据传输
- 服务端得到数据后,Handler做处理
- 如客户端需要反馈结果,那需要客户端也建立Messenger对应的Hander。而服务端在Handler处理时,通过mMessage.replyTo得到客户端的Messenger,然后通过mMessenger.send(mReplyMessage)将result发给客户端。最后客户端在Handler中得到result。
2.4.4 使用AIDL
大致流程:首先建一个Service和一个AIDL接口,接着创建一个类继承自AIDL接口中的Stub类并实现Stub类中的抽象方法,在Service的onBind方法中返回这个类的对象,然后客户端就可以绑定服务端Service,建立连接后就可以访问远程服务端的方法了。
注意:
- AIDL的包结构在服务端和客户端要保持一致,否则会运行出错
- AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时运行时,会出现多个线程同时访问的情形。因此AIDL方法要处理好线程同步。比如List,建议使用CopyOnWriteArrayList来进行自动的线程同步,类似的还有ConcurrentHashMap。当AIDL传递时,Binder会按照List和Map的规范最形成ArrayList和Hashmap传递给客户端。
- 观察者模式:客户端注册一个listener到服务端,用于观察服务端的状态。
- 需要加一个listener的aidl文件,并且要在原本的aidl文件中加registerlistener/unRegisterListener的接口
- 客户端registerListener
- 服务端的listener保存要使用RemoteCallbackList
- 在需要的时机,服务端运行
final int N = mRemoteCallbackList.beginBroadcast(); for (int i = 0; i < N; i++) { IxxxListener l = mRemoteCallbackList.getBroadcastItem(i); if (l != null) { try { l.onXxxed(); } catch (RemoteException e) { e.printStackTrace(); } } } mRemoteCallbackList.finishBroadcast();
RemoteCallbackList是系统专门提供的用于删除跨进程Listener的接口。RemoteCallbackList是一个泛型,支持管理任意的AIDL接口,因为所有的AIDL接口都继承自IInterface接口。
- 远程调用的方法是运行在Binder线程池,同时客户端线会挂起,如果远程调用耗时,则会导致客户端线程阻塞,此时要避免UI线程进远程调用,尤其是onServiceConnected/onServiceDisConnected。
- 服务端方法是运行在Binder线程池中的,所以原本就可以执行大量耗时操作,因此不需要开新线程进行异步任务。
- 当服务端调用客户端listener中的方法时,调用的方法是运行在客户端的Binder线程池,因此如果此调用耗时,会导致服务端线程阻塞无法响应。
- Binder是可能意外死亡的,往往是因为服务端进程意外终止了。重练远程服务的方法:
- 死亡代理:DeathRecipient监听,在binderDied中重连
- onServiceDisconnected中重连。
2.4.5 使用ContentProvider
- ContentProvider主要以表格的形式来组织数据,并且可以包含多个表;
- ContentProvider还支持文件数据,比如图片、视频等,系统提供的MediaStore就是文件类型的ContentProvider;
- ContentProvider对底层的数据存储方式没有任何要求,可以是SQLite、文件,甚至是内存中的一个对象都行;
- 要观察ContentProvider中的数据变化情况,可以通过ContentResolver的registerContentObserver方法来注册观察者;
- 跨进程访问ContentProvider的数据,只需要通过ContentResolver的query、update、insert、delete方法。
- sqliteDatabase内部对数据库的操作是有同步处理的。
2.4.6使用Socket
2.5 Binder连接池
将所有的AIDL放在同一个Service中去管理是一种比较好的方式。
2.5.1 Binder连接池工作机制:
- 每个业务模块创建自己的AIDL接口并实现此接口,这个时候不同业务模块之间是不能有耦合的,所有实现细节我们要单独开来,然后向服务端提供自己的唯一标识和其对应的Binder对象;
- 对于服务端来说,只需要一个Service,服务端提供一个queryBinder接口,这个接口能够根据业务模块的特征来返回相应的Binder对象给它们,不同的业务模块拿到所需的Binder对象后就可以进行远程方法调用了。
2.5.2 Binder连接池的主要作用
- 将每个业务模块的Binder请求统一转发到远程Service去执行,从而避免了重复创建Service的过程。
- 建议在AIDL开发工作中引入BinderPool机制
作者的BinderPool源码