如何定义一个 AIDL 接口并实现跨进程调用?需要哪些步骤?

解读

在国内面试中,这道题几乎 100% 会出现在“Framework 层”环节,用来快速筛掉只写过 UI 的“API 调用工程师”。面试官真正想听的是:

  1. 你对 Binder 本质的理解(一次拷贝、内核驱动、C/S 架构);
  2. 你是否亲手写过 AIDL、编译过 stub、处理过 RemoteException;
  3. 你是否知道 Android 11 以后“包可见性”对隐式启动 Service 的影响;
  4. 你是否能把“线程安全”“Parcelable 序列化”“死亡代理”这些细节讲清楚。

回答时切忌只背“四步走”,而要围绕“定义→编译→实现→暴露→调用→异常→优化”七个关键词展开,让面试官听到你“踩过坑”。

知识点

  1. AIDL 语法:支持 Java 基本类型、String、CharSequence、List、Map、Parcelable、AIDL 接口本身;自定义 Parcelable 必须写同名 .aidl 文件 createFromParcel。
  2. Binder 线程池:客户端调用会挂起,服务端默认运行在 Binder 线程池,若需主线程需手动 Handler 切回。
  3. 死亡代理:DeathRecipient,服务端进程被杀后客户端可快速降级,避免卡死。
  4. 版本管理:服务端 oneway、in/out/inout 方向性、接口版本号字段,防止新老客户端混用出现 TransactionTooLargeException。
  5. 安全校验:国内厂商 ROM 对“后台启动 Service”限制极严,需显式 Intent + 包名 + 权限声明,Android 11+ 还要加 queries 标签。
  6. 性能陷阱:Binder 缓冲区 1 MB,复杂对象务必分页或匿名共享内存(Ashmem);避免在循环里调用同步接口。
  7. 替代方案:若仅单向传数据,优先考虑 Broadcast + Intent、Messenger、ContentProvider;AIDL 只在“双向、并发、低延迟”场景下使用。

答案

步骤一:定义接口

  1. 在服务端 module 的 main/aidl/包名/ 目录下新建 ICompute.aidl
    package com.example.aidldemo;
    interface ICompute {
        int add(int a, int b);
    }
    
  2. 若参数含自定义 Bean,需让 Bean 实现 Parcelable,并在同级目录写 Bean.aidl 声明 parcelable Bean;

步骤二:编译生成 Binder 桩
Gradle 同步后,build/generated/aidl_source_dir 下自动生成 ICompute.java,内含 Stub 抽象类与 Proxy 实现。

步骤三:实现服务端

  1. 新建 ComputeService 继承 Service,在 onBind 返回 ICompute.Stub 匿名实现:
    private val binder = object : ICompute.Stub() {
        override fun add(a: Int, b: Int) = a + b
    }
    override fun onBind(intent: Intent) = binder
    
  2. 在 AndroidManifest.xml 显式注册并加 exported="true",Android 11+ 需配套权限:
    <service android:name=".ComputeService"
             android:exported="true"
             android:permission="com.example.COMPUTE">
        <intent-filter>
            <action android:name="com.example.COMPUTE_SERVICE"/>
        </intent-filter>
    </service>
    
  3. 定义权限文件:
    <permission android:name="com.example.COMPUTE"
                android:protectionLevel="signature"/>
    

步骤四:客户端调用

  1. 将服务端 aidl 目录完整拷贝到客户端 main/aidl,保持包名一致;
  2. 在 Activity 中绑定服务:
    private lateinit var computeService: ICompute
    private val connection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName, service: IBinder) {
            computeService = ICompute.Stub.asInterface(service)
            // 注册死亡代理
            service.linkToDeath({ /* 降级逻辑 */ }, 0)
        }
        override fun onServiceDisconnected(name: ComponentName) {
            // 服务端异常被杀
        }
    }
    bindService(
        Intent("com.example.COMPUTE_SERVICE").apply {
            `package` = "com.example.aidldemo"
        },
        connection,
        Context.BIND_AUTO_CREATE
    )
    
  3. 调用远程方法需捕获 RemoteException,且不能在主线程做耗时调用,可用 Kotlin Coroutine + Dispatchers.IO 包裹。

步骤五:验证与签名

  1. 服务端与客户端必须使用同一签名证书,否则 protectionLevel="signature" 权限校验失败;
  2. 通过 adb shell service list | grep compute 查看是否注册成功;
  3. 用 adb shell am startservice 模拟隐式启动,确认国内 ROM 没有“后台弹出”拦截。

拓展思考

  1. 如果业务需要“服务端主动推送”怎么办?
    可在 AIDL 里再定义一个 registerCallback(IComputeCallback cb) 接口,服务端持有 RemoteCallbackList<IComputeCallback>,利用 beginBroadcast / finishBroadcast 实现一对多推送;注意 RemoteCallbackList 内部自动处理 DeathRecipient,防止内存泄漏。

  2. 跨进程大数据传输如何优化?
    超过 500 KB 时考虑匿名共享内存:服务端创建 MemoryFile,返回 ParcelFileDescriptor;客户端通过 SharedMemory 映射读取,避免 Binder 事务过大导致 TransactionTooLargeException。

  3. 如何做单元测试?
    在 androidTest 里使用 ServiceTestRule 启动真实 Service,或通过 Mockito-inline 伪造 Binder,配合 Kotlin Coroutine 测试远程调用超时场景,确保客户端在 3 秒内能降级到本地缓存。

  4. 与 HIDL/AIDL 稳定化接口的区别?
    Android 10 以后 Google 推出“稳定 AIDL”(Stable AIDL),接口一经发布即冻结,用于替换 HIDL。若你做的是系统服务(如 CameraService),需使用 stable AIDL 并写 aidl_interface 模块,通过 Soong 编译成 .so 供 vendor 与 system 双向调用,这与应用层 AIDL 的“源码拷贝”模式完全不同,面试时可作为加分项抛出。