3、AIDL基础:AIDL语法、定义接口、生成Java代码、手动实现Binder接口

好,咱们今天来聊聊AIDL。说实话,很多做Android开发的朋友,可能天天跟AIDL打交道,但真要问起它背后的原理,往往就卡壳了。我记得我刚入行那会儿,也是照着模板写,出了bug根本不知道怎么调。今天我就带你把它彻底搞明白。

3.1 AIDL到底是什么?

AIDL,全称是Android Interface Definition Language,翻译过来就是“Android接口定义语言”。说白了,它就是一套帮你生成Binder通信代码的模板工具。

你想想看,两个进程要通信,数据得序列化、反序列化,还得通过Binder驱动来回传递。这些代码写起来又臭又长,还容易出错。AIDL的作用就是——你只管定义接口,它帮你生成那些繁琐的底层代码。

核心理解:AIDL不是通信协议,而是代码生成器。它生成的Java代码,本质上就是一套基于Binder的IPC实现。

3.2 AIDL语法速览

AIDL的语法其实很简单,跟Java接口很像。我总结了一下,核心就这几点:

  • 包声明:必须跟实际Java包名一致
  • 导入语句:除了默认类型,其他都要显式import
  • 接口定义:用interface关键字,方法可以带参数和返回值
  • 数据类型:支持基本类型、String、CharSequence、List、Map,以及Parcelable对象
  • 方向标记:in、out、inout,这个很重要,后面细说

来看一个完整的例子:

// IStudentManager.aidl
package com.example.school;

import com.example.school.Student;

interface IStudentManager {
    // 添加学生,返回学号
    int addStudent(in Student student);
    
    // 根据学号查询学生
    Student getStudent(int studentId);
    
    // 批量查询,用List返回
    List<Student> getStudentsByIds(in List<Integer> ids);
    
    // 注册监听器,注意oneway关键字
    oneway void registerListener(in IStudentListener listener);
}

个人经验:我在项目中遇到过一个问题——忘记加in/out标记,结果服务端改了对象,客户端完全没感知。后来我养成了习惯,所有非基本类型参数都显式加上方向标记,哪怕默认是in。

3.3 方向标记:in、out、inout

这个知识点,很多教程一笔带过,但我认为它特别重要。为什么?因为它直接影响性能。

标记 含义 数据流向 性能影响
in 客户端→服务端 只传一次 最快
out 服务端→客户端 只传一次 较快
inout 双向传输 传两次 最慢,慎用

嗯,这里要注意:inout意味着数据要来回序列化两次。如果你只是想让服务端改个值再返回,完全可以用out,或者干脆用返回值。我曾经接手过一个项目,里面大量用了inout,一查发现根本没必要,改完之后性能提升了将近30%。

3.4 生成Java代码

写完.aidl文件,接下来就是生成Java代码。在Android Studio里,这步是自动的。你只需要:

  1. 把.aidl文件放到 src/main/aidl/ 目录下
  2. 确保包名路径正确
  3. Build一下,代码就生成了

生成的代码在 build/generated/source/aidl/ 目录下。我建议你打开看看,里面包含了:

  • 接口文件:继承自IInterface
  • Stub类:抽象类,继承自Binder,实现了接口
  • Proxy类:客户端的代理,负责把调用转成Binder事务

关键点:Stub和Proxy是成对出现的。Stub在服务端,Proxy在客户端。它们通过Binder驱动通信,你完全不用操心底层细节。

3.5 手动实现Binder接口

说实话,AIDL虽然方便,但有时候你不想依赖它生成代码,或者想更灵活地控制Binder通信。这时候,手动实现Binder接口就派上用场了。

我举个例子,假设我们要实现一个简单的加法计算器:

// 1. 定义接口
public interface ICalculator {
    int add(int a, int b);
}

// 2. 服务端实现
public class CalculatorService extends Service {
    
    private final Binder mBinder = new ICalculator.Stub() {
        @Override
        public int add(int a, int b) {
            return a + b;
        }
    };
    
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

// 3. 客户端调用
ICalculator calculator = ICalculator.Stub.asInterface(service);
int result = calculator.add(3, 5);

你看,核心就三步:定义接口、服务端实现、客户端调用。但如果你不用AIDL,手动写的话,就得自己处理:

  • 实现Binder的onTransact方法
  • 手动序列化和反序列化数据
  • 处理线程安全

我曾经在一个性能敏感的项目里,手动实现了Binder接口。为什么?因为AIDL生成的代码里有一些不必要的同步操作,影响了吞吐量。手动实现后,我把锁粒度降到了最低,性能提升了将近一倍。

避坑指南:我曾经犯过一个错误——在手动实现Binder时,忘了处理跨进程的异常传递。结果客户端收到的是TransactionException,根本不知道服务端出了什么问题。后来我养成了习惯,在onTransact里统一捕获异常,并通过writeException传回客户端。

3.6 实战建议

最后,给你几个实战中的建议:

  • 优先用AIDL:除非你有特殊需求,否则别自己造轮子。AIDL生成的代码经过了充分测试,稳定性有保障。
  • 注意线程模型:Binder调用默认是同步的,会阻塞调用线程。如果你在UI线程调Binder,记得用异步方式或者开子线程。
  • 控制数据大小:Binder的传输缓冲区默认是1MB。超过这个大小,会抛出TransactionTooLargeException。我建议单次传输不要超过几百KB。
  • 善用oneway:如果不需要返回值,用oneway关键字。它会让调用变成异步,不阻塞客户端。

好了,这一章的内容就到这里。AIDL说难不难,说简单也不简单。关键是要理解它背后的Binder机制,这样遇到问题才能游刃有余。下一章,我们深入Binder驱动层,看看数据到底是怎么在进程间流动的。