1. Binder 连接池
通过前面几篇文章的介绍,我们知道,不同的 IPC 方式有不同的特点和适用场景。在这篇文章中,我们在介绍下 AIDL,原因是 AIDL 是一种最常用的进程间通信方式,是日常开发中进程间通信的首选,所以我们需要额外强调一下。
如何使用 AIDL 我们在前面已经介绍完了,这里在回顾一下它的大致流程,首先创建一个 Service 和一个 AIDL 接口,接着创建一个类继承自 AIDL 接口中的 Stub 类并实现 Stub 中的抽象方法,在 Service 的 onBind 方法中返回这个类的对象,然后客户端就可以绑定服务端的 Service,建立连接后就可以访问远程服务端的方法了。
上述过程就是典型的 AIDL 的使用流程。这本来没有什么问题,但是现在考虑一种情况:公司的项目越来越大了,现在有十个业务模块都需要使用 AIDL 来进行进程间通信,那么我们该如何处理呢?也许你会说:“就按照 AIDL 的实现方式一个一个的实现呗”,这是可以的,如果用这种方法,我们首先需要创建 10 个Service,这好像有点多啊!如果有 100 个地方需要使用 AIDL 呢,先创建 100 个 Service?到这里,或许我们应该明白问题所在了。随着 AIDL 数量的增加,我们不能无限制的增加 Service,Service 是四大组件之一,本身就是一种系统资源,而且太多的 Service 会使得我们的应用看起来很重量级,因为正在运行的 Service 可以在应用详情页看到,当我们的详情页有十个服务正在运行时,这看起来并不是什么好事,针对上述问题,我们需要减少 Service 的数量,将所有的 AIDL 放在他同一个 Service 中去处理。
在这种模式下,整个工作机制是这样的:每个业务模块创建自己的 AIDL 接口并实现次接口,这个时候不同业务模块之间是不能有耦合的,所以实现细节我们要单独开发,然后向服务端提供自己的唯一标识和其对应的 Binder 对象;对于服务端来说,只需要一个 Service 就够了,服务端提供一个 queryBinder 接口,这个接口能更具业务模块的特征来返回相应的 Binder 对象给他们,不同的业务模块拿到所需的 Binder 对象后就可以进行远程方法调用了。由此可见,Binder 连接池的主要作用就是将每个业务模块的 Binder 请求统一转发到远程 Service 中去执行,从而避免了重复创建 Service 的过程,他的工作原理如下图:
通过上面的理论,也许还有点不好理解,下面对 Binder 连接池的代码实现做一下说明。首先,为了说明问题,我们提供了两个 AIDL 接口(ISecurityCenter 和 ICompute)来模拟上面提到的多个业务模块都要使用 AIDL 的情况,其中 ISecurityCenter 接口提供加解密功能,声明如下:
// ISecurityCenter.aidl
package com.demo.text.demotext.aidl;
interface ISecurityCenter {
String encrypt(String content);
String decrypt(String password);
}
而 ICompute 接口提供计算假发的功能,声明如下:
// ICompute.aidl
package com.demo.text.demotext.aidl;
interface ICompute {
int add(int a, int b);
}
虽然上面两个接口的功能都比较假单,但是用来分析 Binder 连接池的工作原理已经足够了。接着看一下上面两个 AIDL 的实现,也比较简单,代码如下:
public class ISecurityCenterImpl extends ISecurityCenter.Stub {
@Override
public String encrypt(String content) throws RemoteException {
return getMD5(content);
}
@Override
public String decrypt(String password) throws RemoteException {
return encrypt(password);
}
public static String getMD5(String str) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(str.getBytes());
return new BigInteger(1, md.digest()).toString(16);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
public class IComputeImpl extends ICompute.Stub {
@Override
public int add(int a, int b) throws RemoteException {
return a + b;
}
}
现在业务模块的 AIDL 接口定义和实现都已经完成了,注意这里并没有为每个模块的 AIDL 单独创建 Service,接下来就是服务端和 Binder 连接池的工作了。
首先,为 Binder 连接池创建 AIDL 接口 IBinderPool.aidl,代码如下:
// IBinderPool.aidl
package com.demo.text.demotext.aidl;
interface IBinderPool {
IBinder queryBinder(int binderCode);
}
接着,为 Binder 连接池创建远程 Service 并实现 IBinderPool,下面是 queryBinder 的具体实现,可以看到请求转发的实现方法,当 Binder 连接池连接上远程服务时,会根据不同模块的标识即 binderCode 返回不同的 Binder 对象,通过这个 Binder 对象所执行的操作全部发生在远程服务端、
@Override
public IBinder queryBinder(int binderCode) throws RemoteException {
IBinder binder = null;
switch (binderCode) {
case BINDER_SECURITY_CENTER:
binder = new ISecurityCenterImpl();
break;
case BINDER_COMPUTE:
binder = new IComputeImpl();
break;
default:
break;
}
return binder;
}
远程 Service 的实现就比较简单了,代码如下:
public class BinderPoolService extends Service {
private static final String TAG = "BinderPoolService";
private Binder mBinderPool = new IBinderPoolImpl();
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinderPool;
}
}
下面还剩下 Binder 连接池的具体实现,在它的内部首先它要去绑定远程服务,绑定成功后,客户端就可以通过它的 queryBinder 方法去获取各自对应的 Binder,拿到所需的 Binder 以后,不同业务模块就可以进行各自的操作了。Binder 连接池的代码如下所示:
public class IBinderPool {
private static final String TAG = "IBinderPool";
public static final int BINDER_COMPUTE = 0;
public static final int BINDER_SECURITY_CENTER = 1;
private Context context;
private com.demo.text.demotext.aidl.IBinderPool mBinderPool;
private static volatile IBinderPool sInstance;
/**
* 同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信
*/
private CountDownLatch mConnectBinderPoolCountDownLatch;
private IBinderPool(Context context) {
this.context = context;
connectBinderPoolService();
}
public static IBinderPool getsInstance(Context context) {
if (sInstance == null) {
synchronized (IBinderPool.class) {
if (sInstance == null) {
sInstance = new IBinderPool(context);
}
}
}
return sInstance;
}
/**
* 启动服务端 设置最大并行数
*/
private synchronized void connectBinderPoolService() {
mConnectBinderPoolCountDownLatch = new CountDownLatch(1);
Intent service = new Intent(context, BinderPoolService.class);
context.bindService(service, mBinderPoolConnection, Context.BIND_AUTO_CREATE);
try {
mConnectBinderPoolCountDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public IBinder queryBinder(int binderCode) {
IBinder binder = null;
try {
if (mBinderPool != null) {
//调用 AIDL 接口的 queryBinder方法 找到对应的 Binder
binder = mBinderPool.queryBinder(binderCode);
}
} catch (RemoteException e) {
e.printStackTrace();
}
return binder;
}
public static class BinderPoolImpl extends com.demo.text.demotext.aidl.IBinderPool.Stub{
@Override
public IBinder queryBinder(int binderCode) throws RemoteException {
IBinder binder = null;
switch (binderCode) {
case BINDER_SECURITY_CENTER:
binder = new ISecurityCenterImpl();
break;
case BINDER_COMPUTE:
binder = new IComputeImpl();
break;
default:
break;
}
return binder;
}
}
/**
* 处理 Binder 异常停止的问题
*/
private ServiceConnection mBinderPoolConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mBinderPool = BinderPoolImpl.Stub.asInterface(service);
try {
mBinderPool.asBinder().linkToDeath(mBinderPoolDeathRecipient, 0);
} catch (RemoteException e) {
e.printStackTrace();
}
//重置计数器变为0 多个线程同时被唤醒
mConnectBinderPoolCountDownLatch.countDown();
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
private IBinder.DeathRecipient mBinderPoolDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
mBinderPool.asBinder().unlinkToDeath(mBinderPoolDeathRecipient, 0);
mBinderPool = null;
connectBinderPoolService();
}
};
}
Binder 连接池的具体实现已经分析完了,至于里边用到的 CountDownLatch,这里就不详细说明了。它的好处是显而易见的,针对上面的例子,我们只需要创建一个 Service 即可完成多个 AIDL 接口的工作,下面我们来验证一下效果,新创建一个 Activity,在线程中执行如下操作:
private void doWork() {
//初始化
IBinderPool binderPool = IBinderPool.getsInstance(this);
//获取对应的 Binder 对象
IBinder securityBinder = binderPool.queryBinder(IBinderPool.BINDER_SECURITY_CENTER);
//将 Binder 转化成对应的 AIDL 接口
mISecurityCenter = ISecurityCenterImpl.asInterface(securityBinder);
Log.i(TAG, "visit ISecurityCenter");
String msg = "Hello World 安卓";
try {
String password = mISecurityCenter.encrypt(msg);
Log.i(TAG, "encrypt:" + password);
Log.i(TAG, "decrypt:" + mISecurityCenter.decrypt(password));
} catch (RemoteException e) {
e.printStackTrace();
}
Log.i(TAG, "visit IComputeImpl");
//获取对应的 Binder 对象
IBinder computeBinder = binderPool.queryBinder(IBinderPool.BINDER_COMPUTE);
//将 Binder 转化成对应的 AIDL 接口
mCompute = IComputeImpl.asInterface(computeBinder);
try {
Log.i(TAG, "3+5=" + mCompute.add(3, 5));
} catch (RemoteException e) {
e.printStackTrace();
}
}
在上述代码中,我们先调用了 ISecurityCenter 和 ICompute 这两个 AIDL 接口中的方法,看一下 log,很显然,工作正常。
09-18 15:31:33.467 8059-8073/com.demo.text.demotext I/IBinderPoolActivity: visit ISecurityCenter
09-18 15:31:33.469 8059-8073/com.demo.text.demotext I/IBinderPoolActivity: encrypt:d043d5a0b4e48253c6a67d8ee16a23d2
09-18 15:31:33.470 8059-8073/com.demo.text.demotext I/IBinderPoolActivity: decrypt:2f404b7110963654d94f38795110aea4
09-18 15:31:33.470 8059-8073/com.demo.text.demotext I/IBinderPoolActivity: visit IComputeImpl
09-18 15:31:33.473 8059-8073/com.demo.text.demotext I/IBinderPoolActivity: 3+5=8
这里额外说明一下,为什么要在线程中执行呢?这是因为在 Binder 连接池的实现中,我们通过 CountDownLatch 将 bindService 这一异步操作转换成了同步操作,这就意味着它有可能是耗时的,然后就是通过 Binder 方法的调用过程也有可能是耗时的,因此不建议放在主线程中去执行。注意到 BinderPool 是一个单利实现,因此在同一个进程中只会初始化一次,所以如果我们提前初始化 BinderPool,那么可以优化程序的体验,比如我们可以放在 Application 中提前对 BinderPool 进行初始化,虽然这不能保证当前我们调用 BinderPool 时它一定是初始化好的,但是大多数情况下,这种初始化工作(绑定远程服务)的时间开销是可以接受的。另外,BinderPool 中有断线重连的机制,当远程服务异常终止,也需要手动去重新获取最新的 Binder 对象,这个是需要注意的。
有了 BinderPool 可以大大方便日常的开发工作,比如如果有一个新的业务模块需要添加新的 AIDL,那么在它实现了自己的 AIDL 接口后,只需要修改 BinderPoolImpl 中的 queryBinder 方法,给自己添加一个新的 binderCode 并返回对应的 Binder 对象即可,不需要做其他的修改,也不需要创建新的 Service。由此可见,BinderPool 能够极大的提高 AIDL 的开发效率,并且可以没变大量的 Service 创建,因此,建议在 AIDL 开发中引入 BinderPool 机制。
2.选用合适的 IPC 方法
在前面,我们介绍了各式各样的 IPC 方式,那么到底它们有什么不同呢?我们到底该使用哪一种呢?这里我们来解答下这个问题。如下表,可以明确的看出不同的 IPC 方式的优缺点和适用场景。那么在实际开发中,只需要选择合适的 IPC 方式就可以轻松完成多进程的开发。
IPC 方式的优缺点和适用场景
名称 | 优点 | 缺点 | 适用场景 |
Bundle | 简单易用 | 只能传输 Bundle 支持的数据类型 | 四大组件的进程间通信 |
文件共享 | 简单易用 | 不适合高并发场景,并且无法做到进程间的即时通信 | 无开发访问情形,交换简单的数据实时性不高的场景。 |
AIDL | 功能强大,支持一对多并发通信,支持实时通信。 | 使用复杂,需要处理好县城同步 | 一对多通信,且有 RPC 需求。 |
Message | 功能一般,支持一对多串行通信,支持实时通信。 | 不能很好地处理高并发情形,不支持 RPC, 数据通过 Message 进行传输,因此只能传说 Bundle 支持的数据类型 | 低并发的一对多即时通信,无 RPC 需求,或者无需要返回结果的 RPC 请求。 |
ContentProvider | 在数据源访问方面数据强大,支持一对多并发数据共享,可通过 Call 方法扩展其他操作。 | 可以理解为受约束的 AIDL,主要提供数据的 CRUD 操作。 | 一对多的进程间数据共享。 |
Socket | 功能强大,可以通过网络传输字节流,支持一对多并发实时通信。 | 实现细节稍微有点繁琐,不支持直接的 RPC. | 网络数据传输。 |