12. 权限管理机制(下):PermissionController、AppOps、权限组与特殊权限
好,我们继续聊权限管理。上一章我们把 Android 权限模型的骨架讲清楚了——从 AndroidManifest 声明到 PMS 校验,再到运行时弹窗。但说实话,那只是「前台」逻辑。真正让权限系统变得复杂且强大的,是今天要聊的这几个幕后角色:PermissionController、AppOps、权限组,还有那些让人头疼的特殊权限。
我个人习惯把权限系统比作一个三层过滤网。第一层是 PMS 的静态检查,第二层是 PermissionController 的动态交互,第三层是 AppOps 的运行时审计。嗯,今天我们就重点拆解后两层。
12.1 PermissionController:权限弹窗背后的那个 App
你可能以为权限弹窗是系统 UI 的一部分,比如 SystemUI 或者 Settings。其实不是。从 Android 6.0 开始,Google 把权限请求的 UI 逻辑抽成了一个独立的 APK——PermissionController。它本质上就是一个普通应用,只不过拥有系统级签名权限。
为什么这么做?说白了,就是为了方便厂商定制和 OTA 升级。你想想看,如果权限弹窗逻辑写死在 framework 里,厂商想改个样式或者加个功能,就得改系统源码,牵一发动全身。现在好了,直接替换 PermissionController 这个 APK 就行。
核心职责:
- 处理运行时权限请求(requestPermissions 回调)
- 管理权限开关界面(设置里的应用权限页)
- 处理权限撤销逻辑(比如长时间未使用自动撤销)
- 管理特殊权限的授权流程(悬浮窗、修改系统设置等)
我在项目中遇到过一个问题:某款定制 ROM 的权限弹窗总是闪一下才消失。查了半天,发现是 PermissionController 的 Activity 启动模式配错了,导致每次请求都重建实例。嗯,这种问题其实挺常见的,尤其是厂商做深度定制的时候。
来看一段核心调用链。当应用调用 Activity.requestPermissions() 时,实际流程是这样的:
// 应用端
ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.CAMERA}, 100);
// Framework 端 - Activity.java
public final void requestPermissions(String[] permissions, int requestCode) {
// 实际上会通过 ActivityManagerService 转发给 PermissionController
ActivityManager.getService().requestPermissions(
mToken, permissions, mPackageName, requestCode);
}
// PermissionController 端 - GrantPermissionsActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 解析传入的权限列表
String[] permissions = getIntent().getStringArrayExtra(
Intent.EXTRA_PERMISSIONS);
// 检查权限组策略、显示弹窗、记录结果
showPermissionDialog(permissions);
}
注意,PermissionController 在显示弹窗前,会先检查权限组策略。如果同一个权限组里的某个权限已经被授予了,那其他权限会自动通过,不会弹窗。这就是我们接下来要聊的权限组逻辑。
12.2 权限组:为什么你的应用只弹一次窗?
你有没有想过一个问题:为什么你的应用同时申请 CAMERA 和 RECORD_AUDIO,但系统只弹了一次窗?
答案就是权限组(Permission Group)。Android 把功能相近的权限归到一个组里。比如 CAMERA 和 RECORD_AUDIO 都属于 MICROPHONE 组。用户只要授权一次,组内所有权限都自动通过。
| 权限组 | 包含权限 | 说明 |
|---|---|---|
| CALENDAR | READ_CALENDAR, WRITE_CALENDAR | 日历读写 |
| CAMERA | CAMERA | 相机(注意:麦克风不在这个组) |
| CONTACTS | READ_CONTACTS, WRITE_CONTACTS, GET_ACCOUNTS | 联系人相关 |
| LOCATION | ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, ACCESS_BACKGROUND_LOCATION | 位置信息 |
| MICROPHONE | RECORD_AUDIO | 麦克风 |
| PHONE | READ_PHONE_STATE, CALL_PHONE, READ_CALL_LOG, 等 | 电话相关 |
| STORAGE | READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE | 存储(Android 10+ 已拆分) |
这里有个坑,我曾经踩过。Android 10 之后,ACCESS_BACKGROUND_LOCATION 被单独拎出来了,虽然它和前台定位权限在同一个组,但系统会额外弹一次窗。为什么?因为后台定位涉及用户隐私更敏感,Google 觉得不能「一次授权终身有效」。所以即使权限组相同,特殊权限还是会有独立弹窗。
注意:权限组只在运行时权限弹窗时起作用。如果你用 adb 命令或者通过 Settings 手动授权,系统不会检查权限组策略。换句话说,你可以单独授予组内的某个权限而不影响其他权限。
12.3 AppOps:权限的「运行时审计员」
好,接下来聊一个比较底层的东西——AppOps。说实话,很多 Android 开发者甚至不知道它的存在。但如果你做过系统级开发或者安全分析,你一定绕不开它。
AppOps 的全称是 Application Operations,它本质上是一个权限使用记录和拦截框架。PMS 负责「你有没有这个权限」,AppOps 负责「你实际用没用这个权限,以及允不允许你用」。
我举个例子你就明白了。假设你的应用有 CAMERA 权限,但用户通过系统设置把「相机」的 AppOps 开关关掉了。这时候你调用 Camera.open(),系统会直接返回 null 或者抛异常,即使 PMS 检查权限是 GRANTED 的。
AppOps 的架构其实很简单,核心是一个 Service:
// frameworks/base/services/core/java/com/android/server/appop/AppOpsService.java
public class AppOpsService extends IAppOpsService.Stub {
// 存储每个应用的 op 状态
final SparseArray<SparseArray<Op>> mUidOps = new SparseArray<>();
// 检查某个操作是否允许
public int checkOperation(int code, int uid, String packageName) {
// 先查缓存,再查持久化存储
Op op = getOpLocked(code, uid, packageName);
if (op == null) {
return MODE_ALLOWED; // 默认允许
}
return op.mode;
}
// 记录操作使用情况
public void noteOperation(int code, int uid, String packageName) {
// 写入日志,用于「权限使用记录」页面展示
// 同时触发权限使用监听回调
}
}
每个 AppOps 操作都有一个唯一的 code,比如 OP_CAMERA = 26,OP_RECORD_AUDIO = 27。系统在关键路径上会调用 noteOperation() 来记录使用情况,同时调用 checkOperation() 来判断是否拦截。
我个人习惯在分析权限问题时,先查 AppOps 状态。比如用户反馈「明明给了权限,但功能用不了」,十有八九是 AppOps 被关了。可以用 adb 命令查看:
# 查看某个应用的所有 AppOps 状态
adb shell appops get com.example.app
# 查看相机操作是否被允许
adb shell appops get com.example.app CAMERA
# 设置某个操作为忽略(相当于关闭)
adb shell appops set com.example.app CAMERA ignore
小技巧:AppOps 的 MODE_IGNORED 和 MODE_DENIED 是有区别的。MODE_IGNORED 是静默拒绝——系统不会抛异常,但操作会失败;MODE_DENIED 会直接抛 SecurityException。大多数系统设置里的开关用的是 MODE_IGNORED,所以你的应用可能连崩溃日志都看不到。
12.4 特殊权限:那些不走寻常路的权限
最后聊聊特殊权限(Special Permissions)。这类权限不通过普通的运行时弹窗授权,而是需要用户主动跳转到系统设置页面手动开启。常见的特殊权限包括:
- SYSTEM_ALERT_WINDOW(悬浮窗)
- WRITE_SETTINGS(修改系统设置)
- REQUEST_INSTALL_PACKAGES(安装未知来源应用)
- MANAGE_EXTERNAL_STORAGE(管理所有文件,Android 11+)
- POST_NOTIFICATIONS(通知权限,Android 13+)
为什么这些权限要特殊处理?说白了,因为它们太危险了。悬浮窗可以覆盖在其他应用之上,修改系统设置可以改变整个设备的行为。Google 觉得不能让应用随便弹个窗就拿到这些权限,必须让用户主动去设置里找。
我曾经遇到过一个案例:某个应用在 Android 11 上无法申请 MANAGE_EXTERNAL_STORAGE 权限,用户反馈说「设置里根本没有这个开关」。查了半天发现,这个权限需要满足特定的 use-case 声明,并且在 Google Play 上架时需要审核。如果应用没有在清单文件里声明 android:requestLegacyExternalStorage="true" 或者 MANAGE_EXTERNAL_STORAGE 的权限使用说明,系统根本不会显示这个开关。
特殊权限的申请流程是这样的:
// 检查是否已经授权
if (Settings.canDrawOverlays(context)) {
// 已经有悬浮窗权限
} else {
// 跳转到设置页面
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + context.getPackageName()));
startActivity(intent);
}
// 在 onActivityResult 中检查授权结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_OVERLAY) {
if (Settings.canDrawOverlays(this)) {
// 用户授权了
} else {
// 用户拒绝了,这里不要反复弹窗,会惹恼用户
}
}
}
避坑指南:特殊权限的授权结果无法通过 onRequestPermissionsResult 回调获取。你必须用对应的 API(如 Settings.canDrawOverlays())在 onActivityResult 里检查。另外,不要在用户拒绝后立即再次跳转设置页,至少等一段时间或者给个友好的提示。
12.5 知识体系总览
为了让你更直观地理解这三层过滤网的关系,我画了一张图:
从这张图你可以看到,一个权限操作要成功,必须通过三层检查。PMS 是第一道门,PermissionController 是第二道,AppOps 是第三道。很多开发者只关注第一层,结果出了问题怎么都查不到原因——其实就是忽略了 AppOps 的存在。
嗯,关于权限管理的下半部分就聊到这里。权限组的设计让用户体验更流畅,AppOps 让系统有了更细粒度的控制能力,特殊权限则是对高风险操作的额外保护。这三者配合起来,才构成了 Android 完整且复杂的权限管理体系。
公众号:蓝海资料掘金营,微信deep3321