理解Android的跨进程通信原理的关键是Binder。Binder是Android中独有的跨进程通信方式,底层的支持是Binder Driver,需要知道的是Linux 内核中并不包含Binder Driver,也就是说Linux并不对Android中的Binder跨进程通信方式提供支持。
跨进程通信是需要内核空间作为支持的。传统的IPC机制如管道、Socket都是内核的一部分,因此通过内核支持实现进程间通信自然是没问题的。但是Binder并不是Linux系统内核的一部分,那该怎么办呢?幸好,Linux支持动态内核可加载模块的机制,模块是具有独立功能的程序,它可以被单独的编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样Android就可以动态添加一个内核模块运行在内核空间,用户进程通过这个内核模块作为桥梁实现通信。
在Android系统中,这个运行在内核空间,负责各个用户进程通过Binder实现通信的内核模块就是Binder驱动。
品尝前,建议先阅读:内存映射、Binder原理
本文基于Binder分析Android中的跨进程通信方式 - AIDL。
Android中的Messenger、ContentProvider底层都是基于AIDL,因此,掌握了AIDL的工作原理也就间接掌握了其余的跨进程通讯方案了。
本文资料来源“艺术探索”,重读此书,仅作学习笔记,备忘。
在Android中使用多进程只有一种方法,那就是给四大组件在AndroidManifest中指定android:process属性,除此之外没有其他方法。
关于android:process属性的设置,有两种方式:
进程名以":"开头的进程是属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而进程名不以“:”开头的进程属于全局进程,其他应用通过ShareUID方式可以和它跑在同一个进程中。
小知识点:Android系统会为每一个应用分配一个唯一的UID,具有相同UID的应用才能共享数据。这里需要说明的是,两个应用通过ShareUID跑在同一个进程中是有要求的,需要这两个应用有相同的ShareUID并且签名相同才可以。在这种情况下,它们可以相互访问对方的私有数据,比如data目录,组件信息等,不管它们是否跑在同一个进程中。当然如果它们跑在同一个进程中,那么除了能共享data目录、组件信息,还可以共享内存数据,或者说它们看起来就像一个应用的两个部分。
Android 会为每一个应用分配一个独立的虚拟机,或者说为每一个进程都分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间。
public class UserManager {
public static int userId = 1;
}
如上代码,我们在MainActivity中将userId赋值为2,在SecondActivity(通过android:process运行在单独的进程中)中打印userId,会发现userId的值是1,而不是2。
一般来说,使用多进程会造成如下几个方面的问题:
第一个问题和第二个问题原因类似。
第三个问题是因为SP不支持两个进程同时去执行写操作,否则会导致一定几率的数据丢失,因为SP底层是通过读/写XML文件来实现的,并发写显然是可能出问题的,甚至并发读/写都有可能出问题。
第四个问题也是显而易见的,由于系统要在创建新的进程的同时分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程。因此,相当于系统又把这个应用重新启动了一遍,既然重新启动了,那么自然就会创建新的Application。即运行在同一个进程中的组件是属于同一个虚拟机和同一个Application的,同理,运行在不同进程中的组件是属于两个不同的虚拟机和Application的。
小结:以上问题产生的原因是多进程模式中,不同的进程拥有独立的虚拟机、Application、内存空间。
Binder是Android中的一个类,实现了IBinder接口。
想要具体了解Binder的工作原理:Binder原理。
示例相关代码:
- Book.java
public class Book implements Parcelable {
private String name;
protected Book(Parcel in) {
name = in.readString();
}
......
}
Book在跨进程通信中使用,因此需要序列化,实现Parcelable接口。Parcelable主要用在内存序列化上。如下两种情况虽然通过Parcelable也是可以的,但是过程会稍微复杂,建议使用Seriallizable。
- Book.aidl
package com.json.ipctest;
parcelable Book;
在AIDL文件中,如果用到了自定义的Parcelable对象,必须新建一个和它同名的AIDL文件。
注:定义如上文件的时候,先定义Book.aidl文件然后按照上述格式书写代码即可。否则,如果先定义Book.java,再定义Book.aidl的时候,AS会提示命名冲突。
- IBookManager.aidl
package com.json.ipctest;
import com.json.ipctest.Book;
interface IBookManager {
List getBooks();
void addBook();
}
自定义的Parcelable对象和AIDL对象必须显式import进来,不管它们是否和当前的AIDL文件位于同一个包内。
--------------------------------------分割线----------------------------------------
AIDL文件定义完成,Android就为我们生成了一个Binder的抽象子类。
AIDL文件的本质:只是Android提供给我们的快速构建满足业务需求Binder的工具。我们完全可以抛开AIDL,手动完成自定义Binder。自定义的方式可以参照AIDL生成的Binder子类。
如下代码所示:
package com.json.ipctest;
public interface IBookManager extends android.os.IInterface {
/**
* Local-side IPC implementation stub class.
* Stub 继承自Binder,因此Stub就是一个Binder
* 自定义Binder需继承android.os.Binder,实现android.os.IInterface及用户自定义接口,比如IBookManager。即可。
*/
public static abstract class Stub extends android.os.Binder implements com.json.ipctest.IBookManager {
private static final java.lang.String DESCRIPTOR = "com.json.ipctest.IBookManager";
/**
* Construct the stub at attach it to the interface.
*/
public Stub() {
// 将Binder的唯一标识与Binder绑定,可用于queryLocalInterface
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an com.json.ipctest.IBookManager interface,
* generating a proxy if needed.
*
*/
public static com.json.ipctest.IBookManager asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
// 查询是否为跨进程Binder
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.json.ipctest.IBookManager))) {
return ((com.json.ipctest.IBookManager) iin);
}
return new com.json.ipctest.IBookManager.Stub.Proxy(obj);
}
@Override
public android.os.IBinder asBinder() {
return this;
}
// 该方法运行在服务器端。用于RPC调用
@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
java.lang.String descriptor = DESCRIPTOR;
switch (code) {
case INTERFACE_TRANSACTION: {
reply.writeString(descriptor);
return true;
}
case TRANSACTION_getBooks: {
data.enforceInterface(descriptor);
java.util.List _result = this.getBooks();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addBook: {
data.enforceInterface(descriptor);
this.addBook();
reply.writeNoException();
return true;
}
default: {
return super.onTransact(code, data, reply, flags);
}
}
}
// 若为跨进程通讯,则obj.queryLocalInterface(DESCRIPTOR)返回该内部类
private static class Proxy implements com.json.ipctest.IBookManager {
// mRemote 为BinderProxy
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote) {
mRemote = remote;
}
@Override
public android.os.IBinder asBinder() {
return mRemote;
}
public java.lang.String getInterfaceDescriptor() {
return DESCRIPTOR;
}
@Override
public java.util.List getBooks() throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
// 执行transact,会调用远程服务端的onTransact方法
mRemote.transact(Stub.TRANSACTION_getBooks, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(com.json.ipctest.Book.CREATOR);
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override
public void addBook() throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
}
static final int TRANSACTION_getBooks = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
}
public java.util.List getBooks() throws android.os.RemoteException;
public void addBook() throws android.os.RemoteException;
}
IBookManager.java类结构乍看感觉结构清晰,其实IBookManager.java的实现可以分成两个文件,这样你会发现更容易一眼看明白。我们也可以按照这个步骤,不依赖于AIDL,完全自定义我们需要的Binder。
public interface IBookManager extends android.os.IInterface {
static final java.lang.String DESCRIPTOR = "com.json.ipctest.IBookManager";
static final int TRANSACTION_getBooks = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
public java.util.List getBooks() throws android.os.RemoteException;
public void addBook() throws android.os.RemoteException;
}
public abstrace class BookManager extends android.os.Binder implements IBookManager {
// 重写如下方法
asInterface
asBinder
onTransact
// Proxy 内部类
private static class Proxy implements IBookManager {
// 重写如下方法
asBinder
getInterfaceDescriptor
getBooks
addBook
}
}
使用示例:
public class LocalProcessService extends Service {
@Override
public IBinder onBind(Intent intent) {
return new MyBinder();
}
private class MyBinder extends IBookManager.Stub {
@Override
public List getBooks() throws RemoteException {
Log.e("getBooks", "getBooks");
return null;
}
@Override
public void addBook() throws RemoteException {
Log.e("addBook", "addBook");
}
}
}
分析AIDL生成的IBookManager.java类,IBookManager.java这个类继承自android.os.IInterface,同时它自己也还是个接口。所有可以在Binder中传输的接口都需要继承IInterface接口。
只看上述提出的一堆代码,可能头顶还是一堆问号?
因此,还是老规矩,上图。通过图一目了然,看清把握整体。
第一张:跨进程通信原理
第二张:Binder的工作原理
由2图可知,当客户端发起远程请求时,由于当前线程会被挂起直至服务端进程返回数据。所以如果一个远程方法是很耗时的,那么不能在UI线程中发起此请求;其次,由于服务端的Binder方法是运行在Binder线程池中的,所以Binder方法不管是否耗时都应该采用同步的方式去实现,因为它已经运行在了一个单独的线程中了。
详细分析:
Binder的唯一标识,一般用当前Binder的类名表示;
用于将服务端的Binder对象转换为客户端所需的AIDL接口类型的对象,这种转换过程是区分进程的。如果客户端和服务器端位于同一个进程,那么此方法返回的就是服务端的Stub对象本身,否则返回的是系统封装后的Stub.Proxy对象。
此方法用于返回当前的Binder对象。
这个方法运行在服务端中的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法处理。该方法参数中code可以确定客户端所请求的目标方法是什么,接着从data中取出目标方法所需的参数,然后执行目标方法。当目标方法执行完毕之后,就向reply中写入返回值。如果该方法返回值为false,那么客户端的请求就会失败,利用这一点,我们可以做权限验证,我们也不希望随便一个进程都能远程调用我们的服务。
Binder运行在服务器端进程,如果服务器端由于某种原因异常终止了,这会导致Binder连接断裂,即我们的远程调用失败。如果我们不知道Binder的连接断裂了,那么客户端的功能就会受到影响。为了解决这个问题,Binder提供了linkToDeath和unLinkToDeath。我们可以给Binder设置一个死亡代理,当Binder死亡的时候,我们可以收到通知。
---------------------------分割线--------------------------------------
其他知识点:
1、支持的数据类型:
基本数据类型、String、CharSequence、List、Map、Parcelable、AIDL。
2、除了基本的数据类型,其他数据类型的参数必须标上方向:in - 输入型参数,out - 表示输出型参数、inout - 输入输出型参数
3、AIDL接口只支持方法,不支持声明静态常量。
4、为了方便AIDL开发,建议把所有和AIDL相关的类和文件全部放入同一个包中,这样做的好处是,当客户端是另外一个应用时,我们可以直接把整个包复制到客户端工程中。{AS目前新建AIDL相关已经是在单独的包中了}
5、需要注意的是,AIDL的包结构在服务端和客户端要保持一致,否则运行会出错,这是因为客户端需要反序列化服务端中和AIDL接口相关的所有类,如果类的完整路径不一样的话,就无法成功反序列化,程序也就无法的正常的运行了。
6、AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接的时候,会存在多个线程同时访问的情形,所以我们要在AIDL方法中处理线程同步问题。
---------------------------分割线--------------------------------------
考虑情形:跨进程通讯中的”观察者模式“,需要解决哪些问题?
在日常的开发中使用的解注册方式是没什么问题的,但是放在多进程中就会产生问题,这是为啥呢?因为Binder会把客户端传递过来的对象重新转化并生成一个新的对象。虽然在注册和解注册的时候我们用的是同一个客户端对象,但是通过Binder传递到服务端就会产生两个全新的对象,别忘了对象是不能跨进程直接传输的,对象的跨进程传输本质上都是反序列化的过程。
如何解决?
RemoteCallBackList是系统专门提供用于删除跨进程listener的接口。