做 Android 开发越久,越会发现权限体系不是我们平时看到的“允许/拒绝”那么简单。系统设置里的开关只管一部分,真正在背后记录每一次调用、控制每一次访问的,是一个叫 AppOps 的系统服务。我第一次认真研究它,是想做一个权限使用记录工具,想搞明白某个应用到底在什么时候偷偷开了摄像头、读了位置,结果一打开这个知识缺口就补不完了。
这篇文章就是一次围绕 AppOps 学习过程的完整拆解。AppOps 本质上是一套位于“应用权限之上”的管控层,它决定了一个应用在拿到权限之后,能否真正执行摄像头、录音、定位、读取联系人等敏感操作,同时也会记录这些操作发生的时间和频次。系统设置里的“权限使用情况”页面、权限管理器的后台限制,底层都是 AppOps 在支撑。
这篇文章适合三类人:第一类是刚接触 Android Framework、想搞懂权限体系的开发者;第二类是正在做隐私合规检测或权限审计工具的人;第三类是打算在系统定制、MDM 设备管控方向深入的同学。通读下来你会发现,AppOps 不只是几个 API 的拼装,它把 Android 的权限模型、Binder 通信、系统服务设计、UID 隔离串成了一条完整的链路,值得花时间啃。
1. 先说结论:AppOps 到底在管什么
1.1 一个“权限之上的权限”
如果只用一句话概括,AppOps 就是 Android 权限体系里的“执行闸门”。
正常场景下,应用申请权限时要经过权限弹窗,用户同意后,系统就返回授权。但授权之后应用是否真的可以调用摄像头、录音、读取定位,系统还需要一个更细粒度的开关来兜底。AppOpsManager 就是干这个事的。
比如你在系统设置里把一个应用的“位置信息”权限切到拒绝,表面上看是权限被收回,实际上底层往往是 AppOps 里对应位置操作的 op 被设置为 ignore。系统在应用调用LocationManager时,会先通过 AppOps 校验这个 op 是否被允许,如果被 ignore,就直接返回空数据或者抛异常。
这就解释了为什么很多应用在权限被拒绝后,依然能“正常打开”,但拿不到数据。因为权限层面的拒绝,最终会落到 AppOps 的操作拦截上。
1.2 AppOps 的演进路线
AppOps 并不是一步到位的。我在看 AOSP 源码时梳理了一下它的演进:
- Android 4.3 / 4.4 时期,AppOps 概念被引入,系统内部开始用 op 记录敏感调用,但开发者能调用的接口非常有限。
- Android 7.0 之后,AppOpsManager 里的
checkOp、noteOp、startOp、setMode这些方法逐渐开放,第三方工具开始能做权限使用情况检测。 - Android 9 / 10 前后,系统对权限管理做了大量收紧,包可见性、隐藏 API 限制都开始影响 AppOps 的周边功能。
- Android 10 及以后,系统进一步限制后台访问位置等敏感操作,很多由 AppOps 直接转为 ignore。
- Android 12L 引入了
MODE_FOREGROUND,用于描述“仅前台允许”的状态,让管控粒度更细致。
在学习的时候,不要拿某一版 API 文档去套所有设备。国产 ROM 对 AppOps 的改动更是五花八门,后面我会专门说这个坑。
1.3 为什么值得单独学习 AppOpsManager
很多人觉得系统设置里的“权限使用情况”就是全部了,其实那只是 AppOps 记录结果的可视化。真正有价值的是你掌握 AppOps 之后能做什么:
- 知道一个应用是否真的调用了某个敏感操作,以及上次调用时间。
- 理解系统如何决定“后台执行权限”的开关。
- 在自动化测试里,准确控制权限状态。
- 在企业设备管控里,主动禁用某些应用的敏感能力。
AppOps 直接决定了系统服务的访问入口,它比 ActivityManager、PackageManager 更适合做“行为守门员”。学懂了它,再回头看待权限申请流程,你会发现自己能看到之前看不到的细节。
2. 核心细节解析:AppOps 的权限模型与 API
2.1 从 permission 到 op 的映射机制
AppOps 里最核心的概念就是 op,也就是一次敏感操作。系统把“读取联系人”“打开摄像头”“发送短信”这些行为抽象成几十个 op,每个 op 都有固定的 id 和名称。
应用申请一个运行时权限之后,系统会把权限转换成对应的 op。比如:
| 权限名 | 对应的 op |
|---|---|
| ACCESS_FINE_LOCATION | 精确定位相关 op |
| ACCESS_COARSE_LOCATION | 粗略定位相关 op |
| CAMERA | 摄像头相关 op |
| RECORD_AUDIO | 录音相关 op |
| READ_CONTACTS | 读取联系人相关 op |
| READ_PHONE_STATE | 读取设备信息相关 op |
| READ_EXTERNAL_STORAGE | 读取外部存储相关 op |
这些映射关系在 AOSP 的AppOpsManager.java和permission_flags.xml里都能找到源码实现。你不需要全部背下来,但必须建立“权限和 op 是一一映射”的意识,否则遇到“明明权限开着,却被系统拦截”的情况会一头雾水。
2.2 AppOpsManager 的三个核心维度
看 AppOps 相关的 API,你会发现方法的入参总是围绕三个维度:
- uid:Linux 层面应用的身份标识。系统在安装应用时会给每个应用分配一个 uid,它决定了应用能访问哪些系统资源。
- packageName:包名。AppOps 校验时通常要同时传入 uid 和包名,因为同一个 uid 可能对应多个包,尤其是使用 sharedUserId 的场景。
- op:要检查或修改的操作 ID。
很多初学框架的人会忽略 uid 维度。实际深挖之后你会发现,AppOpsService 内部保存的状态通常以 uid 作为 key,包名只是校验用的辅助信息。
2.3 四种操作模式:allowed、ignored、errored、default
AppOps 对每个 op 都会保存一个 mode,常见的有四类:
| 模式 | 含义 |
|---|---|
| MODE_ALLOWED | 允许执行,系统正常放行 |
| MODE_IGNORED | 忽略,系统直接拒绝操作,但不一定会崩 |
| MODE_ERRORED | 错误,系统返回 SecurityException,应用通常感知最明显 |
| MODE_DEFAULT | 默认,继承系统对普通应用的默认策略 |
从实际表现来看,ignored 模式比 errored 更“柔和”。很多做过位置权限绕过检查的同学都会发现,系统在后台限制位置时,应用拿到的回调结果往往是空值而不是异常,这其实就是 AppOps 的 ignored 模式在起作用。
2.4 如何拿到 AppOpsManager 实例与关键方法
在应用代码里获取 AppOpsManager 非常简单:
val appOps = getSystemService(AppOpsManager::class.java)常用方法包括:
checkOp(op, uid, packageName):只查不记录,无论是否发生过调用,只判断当前是否允许。noteOp(op, uid, packageName):记录一次操作,并检查是否允许。startOp / finishOp:适合持续型操作,比如摄像头预览、录音,操作开始时 start,结束后 finish。setMode(op, uid, packageName, mode):设置某个应用、某个 op 的模式。getPackagesForOps(opIds):获取一批 op 的授权记录。queryOps(mode, flags):按模式条件筛选出所有 op 记录,Android 10 之后的新 API。unsnoozeOps(op, uid, packageName):把系统临时禁止的 op 状态清掉。
方法名字多,但核心路数就一个:传入 op、uid、包名,系统返回这个操作的状态,或者改变这个操作的状态。
3. 实操过程与核心环节实现
3.1 第一步:写一个“权限审计器”,查看系统里现存的 op 记录
我建议你第一步不要急着改任何状态,先做一个只读工具,把系统里某个应用当前的 op 状态拉出来。这样能直观看到 AppOps 到底保存了什么数据。
这里有一个关键前提:普通应用到 Android 10 之后,getPackagesForOps这类接口能查到的东西非常有限。想要完整实验,最省事的途径是:
- 使用 AOSP 模拟器,并且以 root 权限执行调试。
- 自己编译系统,把调试 App 做成系统签名的白名单应用。
- 在 adb shell 里用系统预置的
cmd appops命令做验证。
如果你只用普通真机调试,大概率会发现查不到别的应用的 op 数据,这不是代码写错了,是系统权限本来就收紧。
在可执行的调试环境里,一段最简单的只读代码长这样:
val appOps = getSystemService(AppOpsManager::class.java) // 这里的权限检查需要系统签名或特权应用权限 val packageOps = appOps.getPackagesForOps( intArrayOf( AppOpsManager.OPSTR_CAMERA, AppOpsManager.OPSTR_RECORD_AUDIO, AppOpsManager.OPSTR_FINE_LOCATION ) ) for (pkgOps in packageOps) { Log.d("AppOpsDemo", "package: ${pkgOps.packageName}") for (entry in pkgOps.ops) { Log.d( "AppOpsDemo", "op=${entry.op}, mode=${entry.mode}, " + "time=${entry.time}, duration=${entry.duration}" ) } }OpEntry里的time表示最近一次访问时间,duration表示最近一次持续操作时长。对做隐私审计来说,这两个字段的价值非常高。
3.2 第二步:动态改变某个 op 的结果
如果只是查询,你对 AppOps 的理解还停留在表面。我建议在一个可控的测试应用上做一次 setMode 实验。
比如你想验证“读取联系人”被 ignore 后应用的表现,可以在 adb 环境执行:
adb shell cmd appops set com.example.test READ_CONTACTS ignore之后打开应用,尝试读取联系人,你会发现联系人列表可能返回空,或者抛出 SecurityException,这个异常往往和操作的具体实现有关。
在 Java/Kotlin 层面,如果是特权应用,调用方式类似:
appOps.setMode( AppOpsManager.OPSTR_READ_CONTACTS, uid, packageName, AppOpsManager.MODE_IGNORED )这里要特别提醒:setMode需要系统权限,普通应用直接调用会抛出 SecurityException。学习阶段我不建议你在自己没有掌控权的设备上强行反射绕过,这既违反系统设计,也容易滑向灰色用法。正确的姿势是:在自己的 AOSP 模拟器上,或者在专门做系统开发的测试机上验证。
有一个细节值得记录:直接改 AppOps 的 mode 并不会同步修改运行时权限的授权状态。你会发现系统设置里的“联系人权限”可能还是“允许”,但应用读取时已经拿不到数据,这是 AppOps 层和权限层不完全对齐的经典案例。真正的系统应用在做权限管理时,必须同时处理权限状态和 op 状态,确保 UI 反馈一致。
3.3 第三步:监听权限使用通知
AppOps 不只是被动记录,系统服务在 op 状态变化和访问发生时,还会发送通知。早期可以通过AppOpsManager.OnOpChangedListener监听回调,注册方式如下:
val appOps = getSystemService(AppOpsManager::class.java) appOps.startWatchingMode( AppOpsManager.OPSTR_CAMERA, packageName, object : AppOpsManager.OnOpChangedListener { override fun onOpChanged(op: String, packageName: String) { Log.d("AppOpsDemo", "op $op changed with $packageName") } } )这个方法仍然受系统权限约束,但用来做自己应用的调试很合适。实际场景中,很多“隐私保险箱”类应用并不是通过 AppOps 监听拿到所有数据的,而是结合 Accessibility、UsageStats 等不同通道做行为推断。AppOps 能提供的信号是最底层的,有了它,判断才会更接近真相。
3.4 第四步:用 dumpsys 排查 AppOps 数据
我最推荐的调试方式其实是 adb 配合 dumpsys。它不需要写代码,能快速看到系统内部状态:
adb shell dumpsys appops这条命令会输出当前设备上所有 uid 的 op 状态、模式、最近访问时间等信息。输出内容又长又杂,建议配合 grep 使用:
adb shell dumpsys appops | grep -A 20 com.example.test还可以先查某个 uid 的概况:
adb shell dumpsys appops com.example.test在 AOSP 里,cmd appops也提供了相关子命令,可以先敲:
adb shell cmd appops help不同 ROM 的 help 输出可能不一样,以实际环境为准。日常排查时,我通常先执行dumpsys appops,找到目标包名,再对照系统设置里的“权限使用记录”页面验证结果。如果两边数据不一致,基本可以断定是 ROM 对 AppOps 做了修改。
4. 常见问题与排查技巧实录
4.1 直接调用 AppOpsManager 接口抛 SecurityException
这是初学者最容易撞上的问题。很多人照着老文章里的代码写,在自己的普通应用里调用setMode、getPackagesForOps,运行时直接崩溃。
原因很简单:AppOps 的系统权限约束非常强。AOSP 框架源码里,noteOp、setMode等方法上方都加了@RequiresPermission或者调用前有系统身份判断。普通第三方应用不可能拿到这份权限。
这不是 Bug,是安全设计。如果你只是学习,可以在 AOSP 版本对应源码里看AppOpsManager.java的setMode实现,会发现内部最终要调用mAppOpsService.setMode,而mAppOpsService拿到了Binder.getCallingUid()做校验。
4.2 Android 11 之后查不到其他应用的信息
很多做权限审计工具的人在 Android 11 上发现,PackageManager.getInstalledPackages()返回的结果变少了。这是因为包可见性机制生效。
如果工具确实需要在合规前提下查询已安装应用,要在 Manifest 里声明:
<queries> <intent> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent> </queries>或者申请QUERY_ALL_PACKAGES权限。要注意,这个权限本身在部分应用市场审核里是需要申报理由的,滥用会导致上架风险。
AppOps 学习里最容易忽略的就是“你根本看不到目标应用”,所以必须先处理好包可见性,再谈 op 数据。
4.3 改完模式后,系统又自动恢复
有些同学会发现在自己的系统应用里把某个 op 改成 ignore,过了一段时间它又变回 allowed。这里牵扯到几个可能原因:
- 系统在运行时权限状态变化时,会同步重置 AppOps 状态。
- 用户重新安装了应用,或者清除了应用数据,会导致 AppOps 记录被清空。
- 某些 ROM 在应用自启动或省电策略里做了额外干预,导致 setMode 被覆盖。
碰到这种情况,先确认是不是所有设备都复现,还是只在特定 ROM 上出现。如果是后者,大概率是厂商在 Framework 层改了默认权限管理逻辑。开发阶段不要假设系统完全听你的。
4.4 国产 ROM 对 AppOps 的改动差异
AppOps 在 AOSP 里是标准实现,但国内厂商普遍会做深度定制。比如有的手机会在后台自动拒绝应用被杀后重开的敏感权限;有的会把 AppOps 的 ignore 状态直接反映到设置页面,用户视角上就是“权限被关闭了”。
这种差异不是文档能写完整的,只能通过实测总结。我的习惯是拿到新设备先跑一遍dumpsys appops,看看默认状态有哪些奇怪的 op 被 set 成 ignore。这些“出厂默认值”里藏着很多厂商的策略,也是排查问题的重要线索。
4.5 隐藏 API 与反射学习的边界
很多深想挖 AppOps 细节的人会去反射ServiceManager.getService("appops"),拿到IAppOpsService再调用内部方法。这是学习系统服务结构的有效方式,但要注意,隐藏 API 限制在 Android 9 之后越来越严格,反射不一定每次都能成功。
对于学习场景,我建议你直接在源码里读IAppOpsService.aidl和AppOpsService.java,理解 Binder 那一层的接口设计,比在运行时反射绕过限制更安全、更有长期价值。如果你只是想快速验证,可以在自己的测试机上配合 root 权限执行命令,但不要用这些手段去干扰其他应用。
5. AppOps 的实际应用场景与延伸学习建议
5.1 权限使用监控与隐私合规检测工具
AppOps 最常见的应用场景就是做隐私合规检测。系统设置里的“隐私”页面用的就是 AppOps 数据,具体包括:
- 哪些应用开过摄像头。
- 哪些应用在后台读取过位置。
- 哪些应用最近访问过麦克风。
- 某次操作的持续时长。
如果你想自研一套类似的工具,核心思路就是周期性读取AppOpsManager的查询结果,并记录信息增量。这个方向既能加深对 AppOps 的理解,又能直接落地到合规审计,主观价值很高。
5.2 自动化测试中控制权限状态
做 UI 自动化也要频繁处理权限弹窗。过去很多人选择在测试代码里绕过弹窗,或者用 UiAutomator 点击“允许”按钮。如果测试设备是 AOSP 环境,直接用cmd appops set预置权限状态更省事:
adb shell cmd appops set com.example.test CAMERA allow这样在测试启动前就为应用准备好了权限环境,避免弹窗干扰脚本执行。想要测“拒绝权限”的场景,把 allow 改成 ignore 即可。老的adb shell pm grant只能控制运行时权限,但更深一层的 op 控制还是得靠 AppOps 这一套。
5.3 系统定制与 MDM 设备管控
在企业设备管理、MDM 方案里,AppOps 经常被用来做“能力禁用”。比如某台工作设备不希望任何应用打开相机,管理员可以通过系统策略把相关 op 强制设为 ignore。
这里的实现往往不再由应用层直接调用setMode,而是通过DevicePolicyManager的权限策略、AppOpsManager内部的状态同步来联动。Framework 工程师需要看懂从 API 层到 AppOpsService 的数据流,才能安全地做策略下发。
5.4 从 AppOps 开始,解锁整条 Android Framework 学习路径
学 AppOps 的收获不只是几个 API,它会把很多框架知识点串起来:
- Binder:AppOpsManager 最终要调用 system_server 里的服务。
- 进程与 uid:每个应用的身份隔离影响 op 状态存储。
- 权限模型:Runtime Permission 和 AppOps 的映射关系。
- 系统服务生命周期:AppOpsService 的启动、持久化、重置逻辑。
- 安全设计:为什么不让第三方应用随便改其他应用的 op。
如果你发现自己在看系统设置页的时候开始思考“这个开关底层是哪个 op”“状态是怎么持久化的”,说明你已经摸到 Framework 学习的门道了。
最后再分享一个小技巧
我后来在做项目时发现,很多诡异问题其实是“权限和 op 不同步”导致的。当你下一次遇到“设置里权限明明开着,但功能就是不可用”的情况,先别急着怀疑业务代码,用 adb 执行一下dumpsys appops | grep 目标包名,看看对应的 op 状态。这个动作两秒做完,能替你省下大半天查日志的时间。
AppOps 是一个非常值得反复咀嚼的系统服务。它对权限的管控粒度、对访问行为的记录能力、以及和系统其它模块的联动方式,理解得越深,你对 Android 的全局掌控感就越强。把源码下载下来,直接从AppOpsService.java读起,比背 API 列表有用得多。