4. AIDL接口定义语言:AIDL语法详解、定向Tag、复杂类型传递、AIDL文件编译流程

各位同学,今天我们来聊聊AIDL。说实话,AIDL是车载Android系统里最基础也最核心的跨进程通信工具。我见过不少新人,一上来就照着模板写,结果遇到复杂数据类型传递就翻车。嗯,今天咱们就把AIDL的底裤扒干净。

4.1 AIDL语法详解:从Hello World到生产级

AIDL的语法,说白了就是定义接口的契约。你想想看,两个进程要通信,总得有个双方都认可的协议吧?AIDL就是这个协议。

先看一个最基础的例子:

// IMyService.aidl
package com.example.car;

interface IMyService {
    int add(int a, int b);
    String getCarInfo(String vin);
}

这里要注意几点:

  • 包名必须一致:AIDL文件的包名要和生成的Java文件包名对应。我见过有人把包名写错,编译报错找了半天。
  • 接口名以I开头:这是Android的惯例,不是强制要求,但建议遵守。
  • 方法参数和返回值:支持基本类型、String、CharSequence、List、Map,以及Parcelable对象。
我的习惯:在车载项目中,我会把AIDL文件单独放在一个module里,这样服务端和客户端都能引用。避免重复定义,减少出错概率。

4.2 定向Tag:in/out/inout 到底怎么用?

这是AIDL里最容易让人迷糊的地方。我刚开始做车载项目时,也在这上面栽过跟头。

定向Tag决定了数据在进程间的传递方向:

Tag 含义 适用场景
in 数据从客户端流向服务端 客户端传参数给服务端,服务端只读
out 数据从服务端流向客户端 服务端填充数据,客户端读取
inout 双向流动 客户端传数据,服务端修改后返回

举个例子:

interface ICarService {
    // 客户端传入vin,服务端查询
    CarInfo getCarInfo(in String vin);
    
    // 服务端填充CarInfo对象
    void fillCarInfo(out CarInfo info);
    
    // 客户端传入CarInfo,服务端修改后返回
    void updateCarInfo(inout CarInfo info);
}
我曾经踩过的坑:有一次我写了个out参数,结果客户端传了个null进去,服务端直接崩溃。后来才明白,out参数要求客户端必须传入一个非空对象,服务端才能填充数据。记住:out参数不是让你传null的!

为什么会这样?因为AIDL的底层实现是Binder驱动。in参数会序列化后发送到服务端,out参数会先创建一个空对象,服务端填充后序列化回客户端。inout则是双向序列化,性能开销最大。

我的建议:能用in就别用out,能用out就别用inout。性能开销依次递增。在车载系统中,尤其是高频调用的场景(比如传感器数据),尽量用in。

4.3 复杂类型传递:Parcelable与自定义对象

基本类型好办,但车载系统里经常要传递复杂对象,比如车辆状态、诊断信息、导航路径等。这时候就需要Parcelable了。

步骤很简单:

  1. 定义Java类,实现Parcelable接口
  2. 创建同名的.aidl文件,声明为parcelable
  3. 在接口AIDL文件中使用该类型

看个例子:

// CarInfo.java
public class CarInfo implements Parcelable {
    private String vin;
    private int speed;
    private double batteryLevel;
    
    // 实现Parcelable接口的方法...
}

// CarInfo.aidl
package com.example.car;
parcelable CarInfo;

// ICarService.aidl
import com.example.car.CarInfo;
interface ICarService {
    CarInfo getCarInfo(String vin);
    void setCarInfo(CarInfo info);
}
关键点:Parcelable的读写顺序必须一致。我在项目中遇到过,有人改了Java类的字段顺序,忘了更新Parcelable的读写方法,结果数据全乱了。建议用Android Studio的插件自动生成Parcelable代码。

另外,List和Map也是常用的复杂类型。AIDL支持泛型,但有限制:

  • List<String>、List<Parcelable> 没问题
  • Map<String, Parcelable> 也可以
  • 但List<List<String>> 这种嵌套就不支持了

嗯,这里要注意:如果你需要传递嵌套结构,建议封装成一个Parcelable对象。

4.4 AIDL文件编译流程:从.aidl到.java

很多同学只知道写AIDL,不知道背后发生了什么。其实编译流程很简单:

  1. SDK工具链:Android SDK中的aidl工具会解析.aidl文件
  2. 生成Java接口:生成对应的.java文件,包含Stub和Proxy内部类
  3. 编译Java文件:和普通Java代码一起编译成.dex

具体来说,生成的Java文件结构是这样的:

// 生成的 IMyService.java
public interface IMyService extends android.os.IInterface {
    // Stub类:服务端继承
    public static abstract class Stub extends android.os.Binder implements IMyService {
        // 实现了Binder机制的核心逻辑
        // onTransact() 方法处理IPC请求
    }
    
    // Proxy类:客户端使用
    private static class Proxy implements IMyService {
        // 封装了transact()调用
        // 负责序列化参数、反序列化返回值
    }
}

我在调试车载系统时,经常直接看生成的Java代码。有时候AIDL语法没问题,但生成的代码逻辑不对,这时候就得手动检查Stub和Proxy的实现。

调试技巧:在build/generated/aidl_source_output_dir/目录下可以找到生成的Java文件。如果遇到IPC调用异常,先看看这里生成的代码对不对。

编译流程中还有一个容易忽略的点:AIDL文件的依赖关系。如果你的AIDL引用了其他AIDL中定义的类型,必须确保依赖的AIDL文件也在编译路径中。在Gradle中配置:

android {
    ...
    sourceSets {
        main {
            aidl.srcDirs = ['src/main/aidl', 'src/common/aidl']
        }
    }
}

说白了,AIDL编译就是一次代码生成过程。你写的是接口定义,工具帮你生成跨进程通信的骨架代码。理解了这个,你就能更好地驾驭AIDL。

总结一下

今天的内容不少,我帮你理一下重点:

  • AIDL语法:包名、接口、方法定义,注意基本类型和Parcelable的区别
  • 定向Tag:in是单向输入,out是单向输出,inout是双向。性能依次降低,能用in就别用inout
  • 复杂类型:Parcelable是王道,注意读写顺序一致。List和Map有限制,嵌套结构要封装
  • 编译流程:aidl工具生成Java代码,Stub和Proxy是核心。调试时多看看生成的代码

下一章我们会讲Binder的工作原理,到时候你会更清楚AIDL生成的代码到底在干什么。嗯,今天就到这里,有问题随时问我。