循序渐进 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 中设计有状态的类

相关推荐
ShineWinsu17 分钟前
对于单链表相关经典算法题:206. 反转链表及876. 链表的中间结点的解析
java·c语言·数据结构·学习·算法·链表·力扣
迦蓝叶22 分钟前
JAiRouter 配置文件重构纪实 ——基于单一职责原则的模块化拆分与内聚性提升
java·网关·ai·重构·openai·prometheus·单一职责原则
ST.J24 分钟前
系统架构思考20241204
java·笔记·系统架构
TDengine (老段)44 分钟前
TDengine 时间函数 TIMETRUNCATE 用户手册
java·大数据·数据库·物联网·时序数据库·tdengine·涛思数据
给我个面子中不1 小时前
JUC、JVM八股补充
java·开发语言·jvm
mask哥2 小时前
详解flink性能优化
java·大数据·微服务·性能优化·flink·kafka·stream
hqxstudying2 小时前
Kafka 深入研究:从架构革新到性能优化的全面解析
java·开发语言·微服务·kafka·springcloud
失散133 小时前
并发编程——17 CPU缓存架构详解&高性能内存队列Disruptor实战
java·缓存·架构·并发编程
2501_915918415 小时前
uni-app 项目 iOS 上架踩坑经验总结 从证书到审核的避坑指南
android·ios·小程序·https·uni-app·iphone·webview
游戏开发爱好者85 小时前
iOS 上架 uni-app 流程全解析,从打包到发布的完整实践
android·ios·小程序·https·uni-app·iphone·webview