​Android AIDL 进阶:Service 权限配置与 RemoteCallbackList

QuibblerQuibbler 2020-03-19 约 21 分钟 206 次阅读

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 就既能对外协作,又不怕被滥用或被崩溃的客户端拖垮。

学习资料:

        安卓 AIDL 之 Service 权限配置(CSDN)

        AIDL 中 RemoteCallbackList 的使用及权限验证方式(简书)

相关推荐

精选
ViewPager和PagerAdapter、FragmentPagerAdapter、FragmentStatePager
Android

ViewPager和PagerAdapter、FragmentPagerAdapter、FragmentStatePager

ViewPager1、ViewPagerandroidx.viewpager.widget.ViewPager,Android中使用非常广泛的控件,可以说是APP必备:首次打开引导页、页面Banner广告等。常用方法:setAdapter() 设置适配器setOffscreenPageLimit() 设置缓存的页面个数,默认是 1setCurrentItem() 跳转到特定的页面setOnPage

1.1k
获取Android内置WebView内核版本
Android

获取Android内置WebView内核版本

获取Android内置WebView内核版本竟然能遇到这样奇葩的事情,网页用的技术过于新颖,以至于只支持高版本Chromium内核的,低版本安卓系统中内置的内核版本较低,无法加载前端页面。1、设置查看在系统设置里 > 应用 > 应用管理 > 显示系统应用,查看WebView组件:2、页面查看通过WebView发起的网络请求,都会带上浏览器的UA,通常页面都可以通过UA判断浏览器的内核版本。这里有两

1.1w
Android 14适配总结
Android

Android 14适配总结

Android 14适配总结毕业工作至今已经适配了三个Android大版本,从Android 11到Android 12、再到Android 13。2023年,Google即将推出的Android 14,上半年已经开始第一批适配。现在,第四个Android版本已经适配完,总结记录一下。1、Android 14计划Google一般会在2月份对外发布预告,同时放出开发者预览版。“拉通”各大平台、厂商以

1.1w