2. 权限运行时模型:Android 6.0+ 运行时权限变更与兼容性适配

说实话,Android 的权限机制在 6.0 之前就是个“一次性买卖”——安装时你点一下同意,应用就能为所欲为。但到了 6.0,Google 终于想通了:凭什么你还没用某个功能就得把相机、位置全交出去?于是运行时权限模型来了。

我个人习惯把 Android 权限进化史分成三个时代:安装时授权时代(6.0 以前)、运行时授权时代(6.0 ~ 10)、精细化管控时代(11 以后)。今天咱们就重点聊第二个时代——也是 Mock 回归测试必须啃下来的硬骨头。

核心要点:运行时权限让应用在使用具体功能时弹窗请求授权,用户可以选择「允许」「拒绝」「不再询问」。这意味着你的测试用例必须覆盖所有分支状态,否则线上分分钟暴雷。

2.1 运行时权限的底层变更逻辑

6.0 之后,权限不再是在 AndroidManifest.xml 里声明就能直接用的。你需要动态调用 requestPermissions(),并在 onRequestPermissionsResult() 回调里处理用户的响应。

// 请求单个权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) 
        != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(this, 
            new String[]{Manifest.permission.CAMERA}, 
            REQUEST_CAMERA);
} else {
    // 权限已授予,直接打开相机
    openCamera();
}

嗯,这里要注意:checkSelfPermission 返回的是 PERMISSION_GRANTED 或 PERMISSION_DENIED。但在国产 ROM 上,经常有厂商魔改弹窗逻辑——我在项目里就遇到过小米手机上的权限弹窗点了「拒绝」之后再也弹不出来的情况。

为什么会这样?因为用户勾选了「不再询问」后,onRequestPermissionsResult() 返回的是 PERMISSION_DENIED,但 shouldShowRequestPermissionRationale() 会返回 false。你要通过这个判断来决定是否引导用户去设置里手动打开。

避坑指南:我曾经在测试一个视频录制功能时,只测了「允许」和「拒绝」两种情况,结果上线后有用户反馈点击录制按钮没反应。排查后发现是「不再询问」状态没处理——弹出权限后用户点了拒绝并勾选了不再询问,代码没走引导逻辑。

2.2 兼容性适配策略

你的 App 可能还要支持 5.0 甚至更低的系统。这时候需要按照 targetSdkVersion 和运行时的系统版本做分叉。

系统版本 权限模型 适配方式
Android 5.1 (API 22) 及以下 安装时授权 只需要在清单文件声明权限,无需动态请求
Android 6.0 (API 23) 及以上 运行时授权 必须动态请求,处理回调 & 引导
targetSdkVersion < 23 兼容模式 系统会模拟旧行为——安装时授予所有声明权限

说白了,如果你的 targetSdkVersion 还在 22 或者更低,那 6.0 以上的设备会自动给你所有权限——但别高兴太早,Google Play 从 2018 年起就要求新应用必须 target 26+,所以这只是个临时方案。

我个人建议:不管你的 target 是多少,代码里统一按运行时权限模型写。低版本设备上的 ContextCompat.checkSelfPermission() 会自动返回 PERMISSION_GRANTED(因为安装时已经授予了),这样就一套代码通了。

2.3 权限分组与特殊权限

Android 把权限分成了正常权限和危险权限。正常权限(比如网络、闹钟)系统自动授予,危险权限(比如位置、相机、麦克风)才需要动态请求。危险权限还按组划分——比如「位置」权限组包含 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION,只要组内一个被授予,同组其他权限也自动被授予。

但有个例外:SYSTEM_ALERT_WINDOW(悬浮窗)和 WRITE_SETTINGS(修改系统设置)是特殊权限。它们不走 requestPermissions(),而是要在 Settings 里单独开关,需要用 Settings.canDrawOverlays() 判断,再用 Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION) 跳转。

注意:特殊权限的测试最容易被忽略。我在做 Mock 回归时,就发现某些机型上悬浮窗权限申请后不会弹窗,而是静默拒绝——这个在模拟器上根本复现不了,必须用真机。

2.4 权限状态机与测试用例设计

运行时权限的每个状态都对应一个测试场景。我把状态机画成了图,你看一眼就明白了:

Android 运行时权限状态机 初始状态 (未请求) 已请求 (弹窗中) 已授予 已拒绝 (可再次请求) 不再询问 (永久拒绝) 引导至设置页 再次请求 未处理? 从设置返回 注意:只有「已拒绝」状态时可再次弹窗;「不再询问」后必须引导用户去系统设置里手动授权

基于这个状态机,你的 Mock 回归测试至少要覆盖这 5 条路径:

  1. 首次请求 → 用户允许:最顺畅的用例,确保后续功能正常
  2. 首次请求 → 用户拒绝:检查应用能否优雅降级(比如关闭相机预览、显示占位图)
  3. 首次请求 → 拒绝并不再询问:最容易被遗漏的路径,必须检测 shouldShowRequestPermissionRationale() 为 false 的处理
  4. 拒绝后再次请求 → 允许:验证再次弹窗逻辑是否正常
  5. 从设置页面手动授权后返回:需要模拟 onActivityResult 或生命周期重走

我在 Mock 回归框架里怎么做的?我用 Mockito 模拟 ActivityCompat.requestPermissions() 的行为,然后让被测代码在 onRequestPermissionsResult 回调中收到不同的 grantResults。这样不用真弹窗,速度快,而且能覆盖「不再询问」这种模拟器很难触发的状态。

2.5 兼容性测试的陷阱清单

最后我列几个自己踩过的坑,你写测试用例时千万注意:

  • 不同 ROM 对「不再询问」处理不一致:华为 EMUI 上用户拒绝两次后自动进入「不再询问」;小米 MIUI 上拒绝一次就进入。我的 Mock 测试里统一按「拒绝一次」和「拒绝两次」分两种场景写
  • 权限弹窗的 UI 异步:有些机型上弹窗是异步的,onResume() 可能会在弹窗前调用——别在 onCreate() 里直接检查权限
  • Fragment 中的权限请求:如果用 Fragment.requestPermissions(),回调一定要走到 onRequestPermissionsResult(),Activity 和 Fragment 都有同名方法,别写混了
  • targetSdkVersion 23 与 30+ 的区别:Android 11 开始增加了「仅这一次」选项,权限可以临时授予,用完自动收回。测试时别忘了加这个分支

嗯,运行时权限这东西,说白了就是要你时刻记住:用户随时可能变卦。上一秒给你权限,下一秒可能就去设置里关掉了。你的代码必须能应对任何时刻的权限状态变化——而 Mock 回归测试,正是帮你在发布前兜住这些不确定性的最好手段。