10、AIDL 语言基础:AIDL 的语法、支持的数据类型、定向 tag(in、out、inout)

好,我们开始聊 AIDL。很多同学一听到 AIDL 就觉得头大,觉得这是 Binder 通信里最绕的一环。其实说白了,AIDL 就是一份“接口说明书”。你想想看,两个进程要聊天,总得约定好聊什么、怎么聊吧?AIDL 就是干这个的。

我个人习惯把 AIDL 比作“跨进程的接口定义语言”。它不负责具体干活,只负责描述:我这个服务能提供哪些方法,参数是什么类型,返回值是什么。至于底层怎么序列化、怎么传数据,那是 Binder 驱动的事,AIDL 只负责生成模板代码。

10.1 AIDL 的基本语法

先看一个最简单的例子。假设我们要定义一个图书管理服务的接口:

// IBookManager.aidl
package com.example.aidl;

import com.example.aidl.Book;

interface IBookManager {
    List<Book> getBookList();
    void addBook(in Book book);
}

嗯,这里要注意几点:

  • 包名必须一致:AIDL 文件的包名,要和实际 Java 接口的包名保持一致。我见过不少新手在这里栽跟头,编译报错半天找不到原因。
  • import 语句不能少:如果方法参数或返回值用到了自定义类型(比如上面的 Book),必须显式 import。就算在同一个包下,也得写。
  • 接口名以 I 开头:这不是强制要求,但 Android 源码里都这么干,咱们也入乡随俗。
💡 我的小习惯:每次新建 AIDL 文件,我都会先检查包名。包名错了,后面全白干。

10.2 支持的数据类型

AIDL 不是万能的,它只支持有限的数据类型。我整理了一张表,你一看就明白:

类别 支持的类型 说明
基本类型 int, long, float, double, boolean, byte, char, short 直接支持,不需要任何额外操作
String String 默认定向为 in,不可修改
CharSequence CharSequence 和 String 类似,常用于文本
List ArrayList 或泛型 List 元素类型必须是 AIDL 支持的
Map HashMap 或泛型 Map key 和 value 都必须是 AIDL 支持的
Parcelable 自定义类实现 Parcelable 接口 必须显式 import,且在同包名下
其他 AIDL 接口 IBinder 类型 用于传递 Binder 对象本身
⚠️ 避坑指南:我曾经在项目里试图传一个 java.util.Date 对象,结果编译直接报错。AIDL 不认识 Date,你得自己把它转成 long 时间戳,或者封装成 Parcelable。

为什么会这样?因为跨进程传输时,数据要经过序列化和反序列化。基本类型和 String 是“天生自带序列化”的,而自定义类必须实现 Parcelable 接口,告诉系统怎么拆包、怎么装包。

10.3 定向 tag:in、out、inout

这是 AIDL 里最容易让人迷糊的地方。我刚开始学的时候也绕了好久。其实你只要记住一句话:定向 tag 决定了数据往哪个方向流动

我们画个图来理解:

客户端 (调用方) 服务端 (实现方) Binder 驱动 (内核空间) in 客户端→服务端 out 服务端→客户端 inout = in + out 双向传递

看图说话:

  • in:数据从客户端流向服务端。客户端把数据打包好,服务端收到后直接读。服务端改了参数,客户端那边不会感知到。
  • out:数据从服务端流向客户端。客户端传一个“空壳子”过去,服务端往里面填数据,填完再传回来。
  • inout:双向流动。客户端传数据过去,服务端可以改,改完再传回来。这是开销最大的模式。
🔑 核心原则:能用 in 就别用 out,能用 out 就别用 inout。定向 tag 越宽松,序列化开销越大。我见过有人所有参数都写 inout,结果性能直接腰斩。

看个实际例子就明白了:

// IStudentManager.aidl
interface IStudentManager {
    // in:客户端传学生ID,服务端返回姓名
    String getNameByStudentId(in int studentId);
    
    // out:客户端传空对象,服务端填数据
    void getStudentInfo(out Student student);
    
    // inout:客户端传学生对象,服务端修改后返回
    void updateStudentInfo(inout Student student);
}

这里有个细节:基本类型和 String 默认就是 in,而且不能改成 out 或 inout。为什么?因为基本类型是值传递,你改了副本,原值不会变。String 虽然是引用类型,但它是不可变的,改了也没意义。

💡 我的经验:在项目中,我一般只用 in 和 out。inout 很少用,除非你真的需要双向修改。而且用 inout 时,一定要在文档里写清楚:这个参数会被服务端修改,调用方要注意。

10.4 定向 tag 的底层原理

你可能会问:定向 tag 到底影响了什么?其实它影响的是 Binder 驱动在序列化时的行为。

  • in:只调用 writeToParcel(),把数据写进去。服务端收到后,调用 createFromParcel() 读出来。
  • out:只调用 createFromParcel() 创建一个空对象。服务端改完后,再调用 writeToParcel() 写回去。
  • inout:先 writeToParcel(),再 createFromParcel(),服务端改完后再 writeToParcel(),客户端再 createFromParcel()。来回折腾两趟。

所以你看,inout 比 in 多了一倍的序列化操作。如果你的对象里有很多字段,这个开销就很可观了。

⚠️ 避坑指南:我曾经接手过一个项目,里面有个 AIDL 接口传了一个大对象,所有参数都标了 inout。结果每次调用都要序列化好几 KB 的数据,界面卡顿明显。后来我把不需要双向修改的参数改成 in,性能立马就上来了。

10.5 总结一下

AIDL 其实不复杂,你只要记住三件事:

  1. 语法:包名要对,import 要写,接口名规范。
  2. 数据类型:基本类型、String、List、Map、Parcelable。别传不认识的对象。
  3. 定向 tag:in 是单向传给服务端,out 是服务端填数据传回来,inout 是双向。能用 in 就别用 inout。

嗯,这一节就到这里。下一节我们会聊 AIDL 的实际使用,包括如何定义 Parcelable 类、如何编译 AIDL 文件、以及生成的 Java 接口长什么样。到时候我会拿一个完整的项目例子来讲,保证你一看就懂。


公众号:蓝海资料掘金营,微信deep3321