Android AIDL 笔记

14 阅读7分钟

一、AIDL 是什么?

AIDL 全称 Android Interface Definition Language(安卓接口定义语言)。它是一种让你能够定义客户端与服务端之间通过进程间通信(IPC)进行交互的接口的语言。简单说,它就是你用一套语法规则定义好接口,Android 工具链会自动帮你生成 Binder 通信的模板代码,让你可以像调用本地方法一样,调用另一个进程中的方法。

为什么需要 AIDL?

  • Android 系统中,每个进程有自己的独立内存空间,不能直接访问对方的数据。
  • Binder 是底层的 IPC 机制,但直接使用 Binder 需要手动处理序列化、反序列化、事务码等,非常繁琐。
  • AIDL 是对 Binder 的高层封装,让你只需关注接口定义,不用关心底层细节。

二、AIDL 的基本使用步骤

2.1 定义 AIDL 接口

src/main/aidl 目录下创建 .aidl 文件(包名需与 Java/Kotlin 包名一致或对应)。例如,定义一个 IRemoteService.aidl

// IRemoteService.aidl
package com.example.myserver;

interface IRemoteService {
    // 基本类型
    int getPid();
    
    // 带参数和返回值
    String getData(String key);
    
    // 自定义 Parcelable 对象(需 import)
    import com.example.myserver.User;
    User getUser(int id);
}

注意:

  • AIDL 语法与 Java 类似,支持基本类型、String、CharSequence、List、Map、Parcelable 等。
  • 非基本类型必须显式 import
  • 每个方法默认是同步阻塞的,运行在 Binder 线程池中。

2.2 实现接口(服务端)

在服务端(通常是一个 Service)中实现这个接口:

// RemoteService.kt
class RemoteService : Service() {
    
    private val binder = object : IRemoteService.Stub() {
        override fun getPid(): Int {
            return android.os.Process.myPid()
        }

        override fun getData(key: String?): String {
            return "Data for $key"
        }

        override fun getUser(id: Int): User {
            return User(id, "User$id")
        }
    }

    override fun onBind(intent: Intent?): IBinder {
        return binder.asBinder()
    }
}

关键点:

  • 生成的 Stub 类继承了 Binder 并实现了你的 AIDL 接口。
  • onBind 中返回这个 Binder 对象。

2.3 客户端绑定服务并调用

客户端需要知道服务端的包名和 Service 类名(或使用隐式 Intent + action)。

// ClientActivity.kt
class ClientActivity : AppCompatActivity() {

    private var remoteService: IRemoteService? = null

    private val connection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            // 通过 asInterface 将 IBinder 转换为接口对象
            remoteService = IRemoteService.Stub.asInterface(service)
            // 现在可以调用远程方法了
            val pid = remoteService?.pid
            Log.d("Client", "Remote pid = $pid")
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            remoteService = null
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 绑定服务
        val intent = Intent()
        intent.component = ComponentName("com.example.myserver", "com.example.myserver.RemoteService")
        bindService(intent, connection, Context.BIND_AUTO_CREATE)
    }

    override fun onDestroy() {
        super.onDestroy()
        unbindService(connection)
    }
}

三、AIDL 支持的数据类型

AIDL 对数据类型的支持是有限制的,必须能被序列化跨进程传输:

类型说明
基本类型int, long, boolean, byte, char, float, double
String, CharSequence自动支持
List元素类型必须也是 AIDL 支持的;接收端始终是 ArrayList
Map键值类型必须支持;接收端始终是 HashMap
Parcelable自定义对象需实现 Parcelable 接口,并在同包名下定义同名 .aidl 文件声明
其他 AIDL 接口可以在 AIDL 中传递另一个 AIDL 接口(用于回调)

自定义 Parcelable 对象

  1. 创建 User.javaUser.kt,实现 Parcelable
  2. 在同包名目录下创建 User.aidl 文件(即使里面为空,也需要声明):
// User.aidl
package com.example.myserver;
parcelable User;

四、定向 Tag:in, out, inout

在 AIDL 方法参数中,可以用 inoutinout 修饰,指定参数的数据流向。默认是 in

Tag含义用途
in数据从客户端流向服务端客户端传递参数给服务端,服务端修改不影响客户端
out数据从服务端流向客户端客户端传递一个空对象给服务端,服务端填充后返回给客户端
inout双向流动客户端传递对象,服务端修改后,客户端能感知到修改

示例:

void processData(in String input, out StringBuilder output, inout User user);

经验之谈:

  • outinout 会产生额外的序列化开销,能不用尽量不用。
  • 对于基本类型,定向 tag 无意义,因为它们是值传递。
  • 自定义 Parcelable 才真正体现定向 tag 的作用。

五、实现回调(双向通信)

有时服务端需要主动向客户端发送消息(如监听器模式),这时就需要在 AIDL 中定义回调接口。

  1. 定义回调 AIDL:
// IRemoteCallback.aidl
package com.example.myserver;
import com.example.myserver.User;

interface IRemoteCallback {
    void onUserChanged(User user);
}
  1. 在服务接口中注册回调:
// IRemoteService.aidl
import com.example.myserver.IRemoteCallback;

interface IRemoteService {
    void registerCallback(IRemoteCallback callback);
    void unregisterCallback(IRemoteCallback callback);
}
  1. 服务端管理回调列表:
// RemoteService.kt
private val callbacks = RemoteCallbackList<IRemoteCallback>()

override fun registerCallback(callback: IRemoteCallback?) {
    callback?.let { callbacks.register(it) }
}

override fun unregisterCallback(callback: IRemoteCallback?) {
    callback?.let { callbacks.unregister(it) }
}

// 当有事件发生时,遍历回调
callbacks.broadcast { it.onUserChanged(user) }

关键点:

  • 使用 RemoteCallbackList 管理跨进程回调,它会自动处理进程终止时的清理,避免内存泄漏。
  • 回调方法通常不建议耗时,且应标记为 oneway 以避免阻塞服务端(见下文)。

六、权限验证

为了防止未授权的客户端连接你的 AIDL 服务,可以在 onBind 或方法内部进行权限检查。

6.1 自定义权限

  • AndroidManifest.xml 声明自定义权限:
<permission android:name="com.example.myserver.ACCESS_SERVICE" />
  • 服务端在 onBind 中检查:
override fun onBind(intent: Intent?): IBinder? {
    if (checkCallingPermission("com.example.myserver.ACCESS_SERVICE") == PackageManager.PERMISSION_DENIED) {
        return null
    }
    return binder
}
  • 客户端需在 AndroidManifest.xml 中使用该权限:
<uses-permission android:name="com.example.myserver.ACCESS_SERVICE" />

6.2 检查包名或签名

可以在每个方法内部调用 getCallingUid()getCallingPid(),然后验证 UID 对应的包名是否在白名单中,甚至验证签名是否一致(用于同一个开发者的不同应用)。


七、线程管理与 oneway

  • AIDL 方法是阻塞的:客户端调用远程方法时,当前线程会挂起直到结果返回。如果在 UI 线程调用,容易导致 ANR。因此必须在子线程调用远程方法
  • 服务端方法运行在 Binder 线程池:默认不运行在主线程,所以可以在方法内直接做耗时操作,但注意不要影响其他 Binder 调用。

oneway 关键字

在 AIDL 方法前加 oneway 修饰:

oneway void notifyEvent(String event);
  • 含义:客户端调用后立即返回,不等待服务端执行完成。服务端会接收请求并放入线程池排队,但客户端不会阻塞。
  • 适用场景:回调方法、通知类方法,不希望客户端等待的场景。
  • 限制:oneway 方法不能有返回值,也不能有 outinout 参数。

八、异常处理

所有 AIDL 方法都可能抛出 RemoteException,表示通信过程中发生错误(如服务端进程死亡、Binder 死亡等)。客户端调用时必须捕获或声明抛出。

try {
    remoteService?.someMethod()
} catch (e: RemoteException) {
    // 处理连接异常,比如重新绑定服务
}

此外,可以通过 linkToDeath 监听服务端进程死亡,以便重新连接。


九、AIDL 与 Messenger 对比

特性AIDLMessenger
底层机制基于 Binder,直接生成接口基于 Handler,将消息封装在 Message 中
并发处理支持多线程并发调用消息串行处理(Handler 单线程)
适用场景复杂 IPC、需要多线程、需要高吞吐量简单的跨进程消息传递,不需要高并发
复杂度较高,需定义 AIDL 文件简单,只需创建 Messenger
数据类型支持 Parcelable 等多种类型只支持 Message 能携带的数据(Bundle)

我的选择建议:

  • 如果只是简单的发送一些命令或事件,用 Messenger 更轻量。
  • 如果需要频繁调用、传递复杂对象、并发操作,用 AIDL。

十、实战经验与常见坑

10.1 TransactionTooLargeException

当传输的数据量超过 Binder 缓冲区上限(通常是 1MB)时,会抛出此异常。

  • 避免方法:不要在 AIDL 中直接传递大文件或 Bitmap;改用文件共享、SharedMemory 等方式。
  • 检查数据大小:对于 List/Map 要控制元素数量,或分批传输。

10.2 谨慎使用 inout

inout 会导致对象两次序列化(进出各一次),增加开销。除非真的需要修改对象并返回给客户端,否则用 in 就好。

10.3 接口粒度设计

  • 每个 AIDL 方法尽量职责单一,参数不要太多。
  • 频繁调用的方法尽量设计成轻量级,避免在远程调用中做复杂计算。

10.4 进程死亡与重连

  • RemoteCallbackList 管理回调,它会自动处理进程死亡。
  • 客户端可以监听 IBinder.DeathRecipient,在服务端死亡时尝试重连。

10.5 使用 Kotlin 协程简化

在客户端,可以将 AIDL 调用包装成挂起函数,避免手动切线程:

suspend fun <T> awaitRemoteCall(block: () -> T): T = withContext(Dispatchers.IO) {
    block()
}

然后在协程作用域内调用。


十一、总结

AIDL 是 Android 跨进程通信的基石,虽然入门有一些门槛,但掌握后你会发现它的强大。回想这些年,我用 AIDL 做过音乐播放器的跨进程控制、插件化框架的服务管理、多进程应用的模块通信…… 每次遇到跨进程问题,AIDL 总是首选。

记住几个关键点:

  • AIDL 是接口定义,工具自动生成 Stub。
  • 跨进程调用是同步的,注意线程。
  • 数据序列化有限制,避免大对象。
  • 权限和回调管理要谨慎。