循序渐进 Android Binder(二):传递自定义对象和 AIDL 回调

前言

上一篇文章中,我们介绍了 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 中设计有状态的类

相关推荐
gadiaola26 分钟前
【JVM】Java虚拟机(二)——垃圾回收
java·jvm
coderSong25683 小时前
Java高级 |【实验八】springboot 使用Websocket
java·spring boot·后端·websocket
Mr_Air_Boy4 小时前
SpringBoot使用dynamic配置多数据源时使用@Transactional事务在非primary的数据源上遇到的问题
java·spring boot·后端
豆沙沙包?4 小时前
2025年- H77-Lc185--45.跳跃游戏II(贪心)--Java版
java·开发语言·游戏
年老体衰按不动键盘5 小时前
快速部署和启动Vue3项目
java·javascript·vue
咖啡啡不加糖5 小时前
Redis大key产生、排查与优化实践
java·数据库·redis·后端·缓存
liuyang-neu5 小时前
java内存模型JMM
java·开发语言
UFIT5 小时前
NoSQL之redis哨兵
java·前端·算法
刘 大 望5 小时前
数据库-联合查询(内连接外连接),子查询,合并查询
java·数据库·sql·mysql
怀旧,5 小时前
【数据结构】6. 时间与空间复杂度
java·数据结构·算法