前言
在上一篇文章中,我们介绍了 Binder 的基本概念和基本使用,但是仅仅传递 String 和基本数据类型肯定是不够的,我们需要传递自定义对象,而且目前我们只介绍了客户端程序调用服务端的代码,如果服务端需要调用客户端代码,例如 类似 OnClick 这样的回调我们如何做呢?所以在这一篇文章中。我们将进一步演进上一篇文章中的例子,来解决刚才提到的两个问题。
因此在阅读本文之前,强烈建议先看完上一篇文章:循序渐进 Android Binder(一):IPC 基本概念和 AIDL 跨进程通信的简单实例

将 AIDL 抽出作为单独模块
上一篇的例子中,我们是将 .aidl 文件在客户端和服务端中都放了一份,不过这只是为了演示,在正式的项目中,是不会这么做的。
AIDL 文件就像是定义的 HTTP 协议一样,是客户端和服务端之间共识,因此客户端和服务端必须使用一致的接口声明。AIDL 文件在编译后会生成接口代码(Stub,Proxy 等),这些代码在需要交互的两端保持一致,因此最好的办法就是将其抽出作为独立模块,然后使客户端和服务端都依赖于此模块。
在实际工程中,常见的结构会是这个样子的:
shell
project-root/
├── aidl-interface/ <-- ✅ 独立模块,放置 .aidl 文件
│ └── com/example/IMyService.aidl
│
├── myserver-app/ <-- 服务端
│ └── 依赖 aidl-interface
│
├── myclient-app/ <-- 客户端
│ └── 依赖 aidl-interface
在我们的例子中,项目就会被改造如下的结构:

在新的结构中,我们把 AIDL 文件抽出到 binder_lib 的模块中,然后让 binder_client 和 binder_server 模块依赖这个 binder_lib 模块。这里要注意,因为 binder_lib 模块是用于处理 AIDL 文件,因此需要在其 build.gradle 文件中添加:
gradle
android {
buildFeatures.aidl true
}
将 AIDL 抽出为单独模块,是一种专业、解耦、可靠的方式,也是大型项目中普遍采用的标准做法。因此,强烈建议诸位在实际开发中这么做。
跨进程传递自定义对象
在改造完项目结构之后,我们来解决第一个问题,传递自定义对象。
其实在 AIDL 中传递自定义对象也不是一件麻烦事,只需要遵循一套明确的规则。其核心点在于实现 Parcelable 接口,并在 AIDL 中声明。下面我们创建两个自定义对象,一个 Request 用于从客户端传送给服务端,一个 Response 用于从服务端返回给客户端。我们可以想象一个 HTTP 同步请求的场景,Request 表示请求,Response 表示响应。
下面我们就一步一步来完成这个例子。
步骤一:创建自定义类实现 Parcelable
我们需要将自定义的类实现 Parcelable。本文就不赘述 Parcelable 的用法了,以下给出 Request 类的实现:
java
public class Request implements Parcelable {
private final long requestId;
private final Map<String, String> headerMap;
private final String content;
public Request(long requestId, Map<String, String> headerMap, String content) {
this.requestId = requestId;
this.headerMap = headerMap;
this.content = content;
}
protected Request(Parcel in) {
requestId = in.readLong();
int mapSize = in.readInt();
headerMap = new HashMap<>(mapSize);
for (int i = 0; i < mapSize; i++) {
String key = in.readString();
String value = in.readString();
headerMap.put(key, value);
}
content = in.readString();
}
public static final Creator<Request> CREATOR = new Creator<>() {
@Override
public Request createFromParcel(Parcel in) {
return new Request(in);
}
@Override
public Request[] newArray(int size) {
return new Request[size];
}
};
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(requestId);
if (headerMap != null) {
dest.writeInt(headerMap.size());
for (Map.Entry<String, String> entry : headerMap.entrySet()) {
dest.writeString(entry.getKey());
dest.writeString(entry.getValue());
}
} else {
dest.writeInt(0);
}
dest.writeString(content);
}
@Override
public int describeContents() {
return 0;
}
}
而 Response 类,为了避免代码篇幅过多,就只放出其成员变量:
java
public class Response implements Parcelable {
public final int code;
public final String message;
public final String content;
//......
}
步骤二:创建同名的 .aidl 文件,并声明为 parcelable
在完成 Java 代码之后,需要在 AIDL 中声明这两个类:
java
// Request.aidl
package lic.swift.binder.lib;
parcelable Request;
java
// Response.aidl
package lic.swift.binder.lib;
parcelable Response;
这两个文件将告诉编译器:我要传递 Request 和 Response 的类,它们都是 Parcelable,你去找对应的 Java 实现。
此时有人会想,为什么不能直接在 AIDL 中定义这两个类呢?
这是个好问题,AIDL 本质是接口定义语言,它只用于定义跨进程通信接口的签名(函数名、参数类型),不能用来定义类的实现。因此,我们必须提供 Java 类。
步骤三:在接口中使用自定义类型
前两步只是在声明和定义自定义类,在完成了声明和定义,最后一步就是使用这两个类了。我们在之前的 IComputer.aidl 中增加一个接口:
java
// IComputer.aidl
package lic.swift.binder.lib;
import lic.swift.binder.lib.Request;
import lic.swift.binder.lib.Response;
interface IComputer {
int add(int a, int b);
Response requestResponse(in Request request);
}
此处要注意的是,对于我们自定义的对象,需要先 import ;另外,对于参数 Request,我们需要添加 in 关键字以表明其传输方向。 在完成了接口定义之后,我们需要重新编译一下,以便生成最新的代码。
在服务端,我们需要实现这个接口:
java
public class ComputeService extends Service {
private final static IComputer.Stub computer = new IComputer.Stub() {
@Override
public Response requestResponse(Request request) {
Log.d("avril", "server, stub, requestResponse(request = " + request + "), " + MainActivity.getProcessLog());
return new Response(200, "success", "response content from server.");
}
//......
};
//......
}
在客户端,我们再添加一个按钮做如下访问:
java
findViewById(R.id.button_2).setOnClickListener(v -> {
if (computer == null)
return;
try {
final Map<String, String> map = new HashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");
map.put("key3", "value3");
final Request request = new Request(10000L, map, "request content");
Log.d("avril", "client 远程调用 computer.requestResponse(" + request + "),等待返回...");
final long time = SystemClock.uptimeMillis();
final Response response = computer.requestResponse(request);
Log.d("avril", "client 远程调用返回结果:" + response + ". 耗时:" + (SystemClock.uptimeMillis() - time) + "ms. " + getProcessLog());
} catch (RemoteException e) {
Log.d("avril", "client 远程调用发生异常 RemoteException:\n" + e.getLocalizedMessage());
}
});
再尝试做这种访问,我们能得到如下的 LOG:
shell
lic.swift.binder.client D client 远程调用 computer.requestResponse(Request{requestId=10000, headerMap={key1=value1, key2=value2, key3=value3}, content='request content'}),等待返回...
lic.swift.binder.server D server, stub, requestResponse(request = Request{requestId=10000, headerMap={key1=value1, key2=value2, key3=value3}, content='request content'}), Process.myPid() = 3436, Process.myProcessName() = lic.swift.binder.server, Process.myUid() = 10149, Process.myTid() = 3448, Thread.currentThread().getId() = 39, Thread.currentThread().getName() = binder:3436_3
lic.swift.binder.client D client 远程调用返回结果:Response{code=200, message='success', content='response content from server.'}. 耗时:12ms. Process.myPid() = 3601, Process.myProcessName() = lic.swift.binder.client, Process.myUid() = 10150, Process.myTid() = 3601, Thread.currentThread().getId() = 2, Thread.currentThread().getName() = main
从 log 上能看到自定义的类(Request、Response)是可以使用的了。跨进程传递自定义的类,搞定。
服务端调用客户端回调接口
Binder 不仅可以实现客户端调用服务端(传统 CS 模型),也支持服务端"反向调用"客户端的回调接口 ------ 这在 AIDL 中通过"回调接口注册机制"实现。
这很像是我们给一个 View 设置 OnClickListener,当用户点击时,系统回调 onClick 一样。在 Binder 中就是这样,客户端注册一个回调接口给服务端,服务端在某个时机通过该接口通知客户端。
通过这种类比,我们就知道进行一个远程回调需要哪些步骤了,无非就是定义回调接口,提供注册方法,客户端实现回调并注册给服务端,服务端在需要时回调方法。
此处我们用一个简单的例子来说明。
步骤一:定义客户端回调接口(IClientCallback.aidl)
在 binder_lib 中定义回调接口:
java
// IClientCallback.aidl
package lic.swift.binder.lib;
interface IClientCallback {
void onCallback(String msg);
}
步骤二:添加注册和反注册方法
java
import lic.swift.binder.lib.IClientCallback;
interface IComputer {
void registerCallback(IClientCallback callback);
void unregisterCallback(IClientCallback callback);
//......
}
步骤三:客户端实现回调接口并注册
首先实现一个 IClientCallback.Stub 的对象。
java
private final IClientCallback.Stub clientCallback = new IClientCallback.Stub() {
@Override
public void onCallback(String msg) throws RemoteException {
Log.d("avril", "client 远程回调 onCallback("+msg+"). " + getProcessLog());
}
};
再将这个对象注册到服务端:
java
private final ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
computer = IComputer.Stub.asInterface(service);
try {
computer.registerCallback(clientCallback);
Log.d("avril", "client 已注册回调 registerCallback(clientCallback). " + getProcessLog());
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
};
步骤四:服务端的调用
首先,我们需要实现注册和反注册的接口:
java
private final List<IClientCallback> callbackList = new ArrayList<>();
private final IComputer.Stub computer = new IComputer.Stub() {
@Override
public void registerCallback(IClientCallback callback) throws RemoteException {
callbackList.add(callback);
}
@Override
public void unregisterCallback(IClientCallback callback) throws RemoteException {
callbackList.remove(callback);
}
};
然后在必要的时候,进行调用:
java
private void notifyClients() {
for (IClientCallback callback : callbackList) {
try {
callback.onCallback("callback from server.");
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
这样就完成了服务端调用客户端的代码,也是 Binder 中的回调机制的实现。通过这个例子,大家也应该能够明白回调机制的使用,但是先别着急,这里有一些注意事项。
注意事项
看了上面的例子,有人可能会问:既然将对象注册给了服务端,那能否给这个回调的实现添加一些状态呢?例如:
java
public class MyCallback extends IClientCallback.Stub {
public int callbackCount = 0;
@Override
public void onCallback(String msg) {
callbackCount++;
Log.d("Client", "count = " + count);
}
}
然后服务端是不是可以拿到这个 callbackCount ? 答案是:不行。
因为我们所说 Binder IPC 跨进程传递的本质是代理对象,而不是原始对象本身。因此,对象的状态(成员变量)是无法跨进程共享的,哪怕你看到的是同一个接口。
拿这个例子来说,将客户端向服务端注册一个回调时,Binder 会在客户端保留原始实现,即这个 IClientCallback.Stub 的实现类;而在服务端创建的,只是一个远程通信代理对象,它不能访问客户端类中定义的字段。跨进程调用只能通过 AIDL 接口方法,不能访问对象的成员变量。
总而言之一句话,我们不应该在 AIDL 中设计有状态的类。
