26、Permissions权限管理:运行时权限的请求流程、PermissionsDispatcher与Accompanist对比、权限的测试与模拟、Android 13/14的新权限变化

权限管理,说白了就是App和用户之间的信任博弈。你想想看,一个手电筒应用非要读取你的通讯录,这合理吗?Android从6.0开始引入运行时权限,就是要把这个选择权交还给用户。今天我们就来彻底聊透这个话题。

运行时权限的请求流程

运行时权限的核心逻辑其实不复杂:在需要的时候请求,在拒绝的时候处理。但很多开发者容易忽略一个关键点——用户可能点击“不再询问”。

标准的请求流程应该是这样的:

  1. 检查权限是否已授予
  2. 如果未授予,检查是否需要展示 rationale(解释为什么需要这个权限)
  3. 发起权限请求
  4. 处理回调结果

我在项目中遇到过最典型的坑:直接在Activity的onCreate里一股脑请求所有权限。用户还没搞明白你的App是干嘛的,就先被权限弹窗轰炸。结果呢?大概率是拒绝+勾选“不再询问”。

最佳实践:权限请求应该遵循“即时即用”原则。用户点击“拍照”按钮时,再请求相机权限。这样用户能理解上下文,同意率会高很多。

来看一段标准的请求代码:

// 检查权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
    != PackageManager.PERMISSION_GRANTED) {
    // 需要展示解释吗?
    if (ActivityCompat.shouldShowRequestPermissionRationale(this,
            Manifest.permission.CAMERA)) {
        // 展示一个dialog解释为什么需要相机权限
        showRationaleDialog()
    } else {
        // 直接请求
        ActivityCompat.requestPermissions(this,
            arrayOf(Manifest.permission.CAMERA), REQUEST_CODE_CAMERA)
    }
} else {
    // 权限已授予,直接打开相机
    openCamera()
}

回调处理同样重要:

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<String>,
    grantResults: IntArray
) {
    when (requestCode) {
        REQUEST_CODE_CAMERA -> {
            if (grantResults.isNotEmpty() &&
                grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                openCamera()
            } else {
                // 用户拒绝了
                if (!ActivityCompat.shouldShowRequestPermissionRationale(
                        this, Manifest.permission.CAMERA)) {
                    // 用户勾选了"不再询问"
                    // 引导用户去设置页面手动开启
                    showSettingsDialog()
                }
            }
        }
    }
}

注意:shouldShowRequestPermissionRationale在用户第一次拒绝后返回true,但如果用户勾选了“不再询问”,它会返回false。这个细节很容易搞混,我曾经因为这个bug被测试小姐姐追着改了三版。

PermissionsDispatcher vs Accompanist

手写权限请求代码确实繁琐,所以社区出现了不少封装库。目前最主流的就是PermissionsDispatcher和Accompanist Permissions。我两个都用过,说说我的真实感受。

对比维度 PermissionsDispatcher Accompanist Permissions
实现方式 编译时注解处理(APT) Compose运行时API
依赖复杂度 需要配置kapt/ksp插件 仅需添加依赖
代码侵入性 需要注解标记Activity/Fragment 无侵入,纯函数调用
Compose支持 需要额外适配 原生支持
学习成本 中等(需要理解注解生成逻辑) 低(和Compose风格一致)

我个人习惯:如果项目是传统View体系,选PermissionsDispatcher;如果是Compose项目,无脑选Accompanist

Accompanist的用法非常简洁:

@Composable
fun CameraScreen() {
    val cameraPermission = rememberPermissionState(Manifest.permission.CAMERA)

    Button(onClick = { cameraPermission.launchPermissionRequest() }) {
        Text("打开相机")
    }

    when {
        cameraPermission.status.isGranted -> {
            // 权限已授予,显示相机预览
            CameraPreview()
        }
        cameraPermission.status.shouldShowRationale -> {
            // 显示解释说明
            Text("我们需要相机权限来拍照")
        }
        else -> {
            Text("权限被永久拒绝,请前往设置开启")
        }
    }
}

PermissionsDispatcher则走的是注解路线:

@RuntimePermissions
class MainActivity : AppCompatActivity() {

    @NeedsPermission(Manifest.permission.CAMERA)
    fun openCamera() {
        // 权限已授予,执行操作
    }

    @OnShowRationale(Manifest.permission.CAMERA)
    fun showRationaleForCamera(request: PermissionRequest) {
        // 展示解释说明
        request.proceed() // 用户同意后继续请求
    }

    @OnPermissionDenied(Manifest.permission.CAMERA)
    fun onCameraDenied() {
        // 权限被拒绝
    }

    @OnNeverAskAgain(Manifest.permission.CAMERA)
    fun onCameraNeverAskAgain() {
        // 用户勾选了不再询问
    }
}

我的建议:如果你还在用传统View架构,但又想为未来迁移Compose做准备,可以考虑直接上Accompanist。它其实不依赖Compose UI,核心的权限状态管理逻辑是通用的。

权限的测试与模拟

权限测试是个容易被忽视的环节。很多开发者在模拟器上测试时,直接给了所有权限,结果到了真机上就翻车。

我常用的测试方法:

  • adb命令模拟授权/拒绝adb shell pm grant/revoke 可以快速切换权限状态
  • 测试“不再询问”场景:先用adb revoke权限,然后连续拒绝两次,第三次请求时就会触发永久拒绝逻辑
  • 权限重置adb shell pm reset-permissions 可以把所有权限恢复到初始状态

具体命令示例:

# 授予权限
adb shell pm grant com.example.app android.permission.CAMERA

# 撤销权限
adb shell pm revoke com.example.app android.permission.CAMERA

# 重置所有权限
adb shell pm reset-permissions com.example.app

在自动化测试中,我推荐使用UI Automator配合权限弹窗处理:

@Test
fun testCameraPermission() {
    // 点击触发权限请求的按钮
    onView(withId(R.id.btn_camera)).perform(click())

    // 等待权限弹窗出现并点击"允许"
    val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    device.wait(Until.hasObject(By.text("允许")), 2000)
    device.findObject(By.text("允许")).click()

    // 验证相机已打开
    onView(withId(R.id.camera_preview)).check(matches(isDisplayed()))
}

曾经踩过的坑:在CI环境(如GitHub Actions)中运行UI测试时,权限弹窗可能不会自动出现。解决方案是在测试前先用adb授予所有需要的权限,避免弹窗干扰测试流程。

Android 13/14的新权限变化

每次大版本更新,权限系统都会有一些调整。Android 13和14的变化尤其值得关注。

Android 13(API 33)的核心变化:

  • 细粒度媒体权限:原来的READ_EXTERNAL_STORAGE被拆分为READ_MEDIA_IMAGES、READ_MEDIA_VIDEO、READ_MEDIA_AUDIO三个权限
  • 通知权限:新增POST_NOTIFICATIONS运行时权限,App不能再静默发送通知
  • 附近设备权限:用于蓝牙/Wi-Fi扫描的BLUETOOTH_SCAN权限

Android 14(API 34)的新增限制:

  • 后台位置权限更严格:应用必须在前台才能获取精确位置
  • 照片选择器增强:系统级照片选择器支持更多媒体类型
  • 屏幕共享检测:应用可以检测到屏幕是否被录制或共享

适配Android 13时,最需要注意的是targetSdkVersion升级到33后,如果还使用旧的READ_EXTERNAL_STORAGE,系统会自动降级为只读权限。我有个项目就是因为没及时适配,导致用户反馈“图片加载不出来”,排查了半天才发现是权限问题。

正确的适配方式:

// Android 13+ 使用细粒度权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    val permissions = arrayOf(
        Manifest.permission.READ_MEDIA_IMAGES,
        Manifest.permission.READ_MEDIA_VIDEO
    )
    requestPermissions(permissions, REQUEST_CODE_MEDIA)
} else {
    // 低版本使用旧权限
    requestPermissions(
        arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
        REQUEST_CODE_STORAGE
    )
}

通知权限的适配也需要注意:

// 检查通知权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
        != PackageManager.PERMISSION_GRANTED) {
        // 请求通知权限
        ActivityCompat.requestPermissions(this,
            arrayOf(Manifest.permission.POST_NOTIFICATIONS), REQUEST_CODE_NOTIFICATION)
    }
}

核心原则:不要等到用户抱怨了才去适配新权限。每次Android大版本发布后,第一时间把targetSdkVersion升上去,在模拟器上跑一遍核心流程。我一般会在beta版发布时就做适配,等正式版出来时已经稳如老狗了。

最后,用一张图总结整个权限管理的知识体系:

Android权限管理知识体系 运行时权限请求流程 ① 检查权限状态 ② 展示Rationale ③ 发起权限请求 ④ 处理回调 PermissionsDispatcher • 编译时注解处理(APT) • 适合传统View体系 • 需要配置kapt/ksp插件 Accompanist Permissions • Compose运行时API • 原生Compose支持 • 低学习成本,无侵入 Android 13 变化 • 细粒度媒体权限(图片/视频/音频) • 新增通知权限 POST_NOTIFICATIONS Android 14 变化 • 后台位置权限更严格 • 照片选择器增强

权限管理看似简单,但细节决定成败。从请求流程的设计,到库的选择,再到不同版本的适配,每一步都值得认真对待。记住:尊重用户的选择权,提供清晰的解释,做好异常处理,你的App在权限这块就不会出大问题。

最后分享一个小技巧:在开发阶段,可以在Application的onCreate里临时授予所有权限,方便调试。但上线前一定要删掉这段代码,否则会被Google Play拒审。


公众号:蓝海资料掘金营,微信deep3321