Android AIDL 进阶:Service 权限配置与 RemoteCallbackList
AIDL 让跨进程通信变得简单——定义接口、实现 Stub、bindService 一气呵成。但只要你的 Service 对外暴露,任何应用都能 bind 它、调用它的方法,安全隐患随之而来。本文讲清两个 AIDL 进阶主题:如何给 Service 加权限验证(声明式 + 编程式两种),以及如何用 RemoteCallbackList 安全管理跨进程回调。两者是 "对外提供 AIDL 服务" 的必修课。
1、为什么要做权限验证
AIDL Service 默认是"开放式"的:只要知道 action 或包名,任何 app 都能 bindService 拿到接口代理、调用方法。这在很多场景是不可接受的——比如你的 Service 提供"支付""读通讯录""控制硬件"这类敏感能力,绝不能让陌生应用随便调。所以一旦 Service 要对外,权限验证就是第一道闸门:控制"谁能 bind",甚至"谁能调哪个方法"。
另一个常被忽略的问题是跨进程回调的管理:服务端要主动通知客户端时,会让客户端注册一个 Listener(也是 AIDL 接口)。但"注册—注销"在跨进程下会踩坑(注销失配、内存泄漏、向已死客户端回调崩溃)。这正是 RemoteCallbackList 要解决的。本文先讲权限、再讲回调,配套吃下。
2、AIDL 通信快速回顾
先把 AIDL 的标准骨架过一遍,后面的权限验证就挂在它上面。.aidl 定义接口,编译器生成 Stub(服务端抽象类)与 Proxy(客户端代理):
// IRemoteService.aidl
interface IRemoteService {
int getValue();
void registerListener(IRemoteCallback cb);
void unregisterListener(IRemoteCallback cb);
}
// 服务端:Service 的 onBind 返回 Stub 的实现
private val binder = object : IRemoteService.Stub() {
override fun getValue(): Int = 42
override fun registerListener(cb: IRemoteCallback?) { /* 见第 6 节 */ }
override fun unregisterListener(cb: IRemoteCallback?) { /* ... */ }
}
override fun onBind(intent: Intent?): IBinder? = binder
// 客户端:bindService + ServiceConnection,asInterface 拿到代理
val conn = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val remote = IRemoteService.Stub.asInterface(service)
remote.getValue()
}
override fun onServiceDisconnected(name: ComponentName?) {}
}
bindService(Intent(this, RemoteService::class.java), conn, BIND_AUTO_CREATE)到这里都是"默认开放"的。下面开始给它加锁。
3、权限验证方式一:声明式权限(推荐起步)
最省事的做法,是在 AndroidManifest 里自定义一个权限,并要求"要 bind 本 Service 必须持有它"。这由系统在 bindService 时强制校验,不通过直接抛 SecurityException。
// 服务端 AndroidManifest:声明权限 + 要求 Service 持有
<permission
android:name="com.example.perm.REMOTE_SERVICE"
android:protectionLevel="signature" /> <!-- signature: 同签名才授予,最安全 -->
<service
android:name=".RemoteService"
android:permission="com.example.perm.REMOTE_SERVICE"
android:exported="true"> <!-- 对外暴露 -->
<intent-filter>
<action android:name="com.example.REMOTE_SERVICE" />
</intent-filter>
</service>
// 客户端 AndroidManifest:声明使用该权限
<uses-permission android:name="com.example.perm.REMOTE_SERVICE" />关键字段是 protectionLevel:设为 signature 时,只有与服务端同签名的应用才能获得该权限——这是"自家系列 App 互通、第三方挡在外面"的最佳选择;normal/dangerous 则允许任意应用申请(dangerous 还要用户授权)。声明式权限的优势是系统级强制、零代码、覆盖 bind 整条链;短板是粒度只到"能不能 bind",区分不了"能调哪个方法"——后者要用编程式验证。
4、权限验证方式二:编程式验证(细粒度)
要做"按方法/按调用方"的细粒度校验,就在代码里手动验。关键 API 是 Binder 提供的 getCallingUid() / getCallingPid() / getCallingPackage(),以及 checkCallingPermission / enforceCallingPermission。一个关键细节:onBind 是由 system_server 调用进来的,在 onBind 里拿到的 callingUid 是 system 而非真正的客户端;真正的调用方身份,要在 onTransact(或 Stub 里重写的方法执行时)才能拿到。
private val binder = object : IRemoteService.Stub() {
// 方式 A:按调用方包名白名单校验
override fun getValue(): Int {
val pkg = getCallingPackage() // 真正的客户端包名
if (pkg !in trustedPackages) {
throw SecurityException("未授权调用方: $pkg")
}
return 42
}
// 方式 B:按自定义 permission 校验(checkCallingPermission)
private fun checkCaller(): Boolean {
val ok = checkCallingPermission("com.example.perm.CALL_REMOTE") ==
PackageManager.PERMISSION_GRANTED
if (!ok) throw SecurityException("缺少 CALL_REMOTE 权限")
return true
}
// 方式 C:在 onTransact 最外层统一拦截(所有方法进来都先验)
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
val callerUid = getCallingUid()
if (!isTrustedUid(callerUid)) { // 这里 getCallingUid() 才是真实客户端
return false // 拒绝整次调用
}
return super.onTransact(code, data, reply, flags)
}
}三种思路各有用武之地:白名单包名最直观(适合"只认这几个自家 App");checkCallingPermission 适合"已声明式声明过、再加一道方法级校验";onTransact 拦截适合"所有方法统一鉴权"。生产实践常组合:signature 级声明权限挡住外人 + onTransact 里再验 UID/包名做二次确认,纵深防御。
5、RemoteCallbackList:跨进程回调管理
服务端要主动通知客户端时,会让客户端传一个 IRemoteCallback(AIDL 接口)进来"注册监听"。直觉做法是用 `List<IRemoteCallback>` 存起来,需要时遍历回调。但这在跨进程下会出三个致命问题:
// 直觉写法(有坑):
private val listeners = mutableListOf<IRemoteCallback>() // 用业务对象作 key
// 坑1:客户端每次跨进程拿到的是"新的 Proxy 对象",
// register 时的 cb 和 unregister 时的 cb 是不同对象,equals 不成立 → 注销失败!
// 坑2:客户端进程崩溃后,list 里还留着"死代理",回调时抛 RemoteException
// 坑3:多线程并发 add/remove/遍历,ConcurrentModificationException
// 正确做法:用 RemoteCallbackList<IRemoteCallback>
private val callbackList = RemoteCallbackList<IRemoteCallback>()RemoteCallbackList 是 Android 专门为"跨进程回调注册"设计的容器,它用每个 callback 底层的 IBinder 作为 key(而非业务对象),所以同一个回调无论跨进程拿到几次 Proxy,底层的 IBinder 都相同,register 与 unregister 能正确匹配——这就解决了"注销失配"。同时它内部为每个 callback 注册了 DeathRecipient,客户端一死自动移除,又解决了"回调死代理"。
6、RemoteCallbackList 使用步骤
用法很固定:register/unregister 管理回调,要通知时用 beginBroadcast() / getItem(i) / finishBroadcast() 这套"事务式"遍历。三者必须成对出现:
private val callbackList = RemoteCallbackList<IRemoteCallback>()
override fun registerListener(cb: IRemoteCallback?) {
callbackList.register(cb) // 注册(自动建立 IBinder → callback 映射)
}
override fun unregisterListener(cb: IRemoteCallback?) {
callbackList.unregister(cb) // 按 IBinder 精确注销,不会失配
}
// 服务端要通知所有客户端时:
fun notifyAllClients(data: String) {
val n = callbackList.beginBroadcast() // 开始广播,返回回调数量
for (i in 0 until n) { // i < n
try {
callbackList.getBroadcastItem(i).onUpdate(data) // 逐个回调
} catch (e: RemoteException) {
// 单个客户端调用失败不影响其它;DeathRecipient 会清理死代理
}
}
callbackList.finishBroadcast() // 必须配对调用,结束本次广播
}两条铁律:第一,beginBroadcast 与 finishBroadcast 必须严格配对(在 try/finally 里 finish 更稳),不配对会导致后续广播拿不到数据;第二,遍历期间回调可能抛 RemoteException,要用 try-catch 兜住单个失败,避免一次崩溃中断对所有客户端的通知。
7、客户端死亡自动清理
RemoteCallbackList 最省心的能力,是自动感知客户端进程死亡并清理。它内部为每个注册的 callback 注册了 `IBinder.DeathRecipient`:当客户端进程崩溃或主动 unbind,底层 Binder 连接断开,DeathRecipient 被回调,RemoteCallbackList 会把这条记录自动从列表移除。你不需要手写"心跳"或"手动注销"逻辑。
// 你只管 register/unregister 和 begin/finishBroadcast,
// RemoteCallbackList 内部已经做了:
// - 以 IBinder 为 key 精确匹配(解决注销失配)
// - 注册 DeathRecipient(客户端死亡自动移除,避免回调死代理抛异常)
// - 线程安全(内部加锁,并发 add/remove/遍历不崩)
// 一个小细节:onCallbackDied 可重写,客户端异常死亡时收到通知,做收尾
val callbackList = object : RemoteCallbackList<IRemoteCallback>() {
override fun onCallbackDied(callback: IRemoteCallback?, cookie: Any?) {
// 某个客户端进程挂了,已被自动移除;可在此打日志/做清理
}
}有了这套机制,跨进程"观察者模式"才真正可用——否则自己手写 DeathRecipient、IBinder 映射、并发控制,极易写出 bug。所以凡是"服务端要主动通知多个跨进程客户端"的场景,一律用 RemoteCallbackList,不要自己拿 List 装。
8、完整实战与最佳实践
- 对外 Service 一定加权限:起步用 signature 级声明权限,关键方法再在 onTransact 里二次校验 UID/包名
- 区分"能否 bind"(声明式)与"能否调某方法"(编程式),按敏感度分层
- 跨进程回调一律 RemoteCallbackList;beginBroadcast/finishBroadcast 配对、放 try/finally
- 遍历广播时每个回调单独 try-catch RemoteException,一个客户端失败不连累其它
- 单向通知用 `oneway void onUpdate(...)` 声明,避免服务端阻塞等客户端返回
- AIDL 方法运行在 Binder 线程池,耗时操作要再开工作线程,别占着 Binder 线程
- 客户端 unbind 时记得 unregister,配合 RemoteCallbackList 的 DeathRecipient 双保险
9、总结
对外提供 AIDL Service,必须解决"谁能调"和"怎么安全通知回调"两件事。权限方面,声明式权限(`` + Service `android:permission`,protectionLevel 用 signature)由系统在 bind 时强制校验,挡住陌生人;编程式验证(onTransact 里用 getCallingUid/checkCallingPermission)做方法级细粒度控制。回调方面,RemoteCallbackList 用 IBinder 作 key 精确匹配注销、用 DeathRecipient 自动清理死客户端,是跨进程观察者模式的标准答案。
关键要点:
- 默认 AIDL Service 对任意 app 开放,必须加权限:signature 声明权限 + onTransact 二次校验
- 真实调用方身份在 onTransact/方法里取(getCallingUid),onBind 里拿到的是 system
- RemoteCallbackList 用 IBinder 为 key 解决注销失配;DeathRecipient 自动清理死客户端
- beginBroadcast/finishBroadcast 必须配对;遍历时单独 try-catch RemoteException
- 通知类方法用 oneway;AIDL 方法跑在 Binder 线程池,耗时操作要另开线程
对于做系统层、SDK 层或"跨应用能力开放"的 Android 开发者而言,AIDL 是绕不开的 IPC 利器,而"权限验证 + RemoteCallbackList"正是把它从"能跑的 demo"推向"敢上生产的对外服务"的两块基石。把"signature 权限挡外人、onTransact 验身份、RemoteCallbackList 管回调"这三板斧备齐,你的 AIDL Service 就既能对外协作,又不怕被滥用或被崩溃的客户端拖垮。
学习资料:

