26、Permissions权限管理:运行时权限的请求流程、PermissionsDispatcher与Accompanist对比、权限的测试与模拟、Android 13/14的新权限变化
权限管理,说白了就是App和用户之间的信任博弈。你想想看,一个手电筒应用非要读取你的通讯录,这合理吗?Android从6.0开始引入运行时权限,就是要把这个选择权交还给用户。今天我们就来彻底聊透这个话题。
运行时权限的请求流程
运行时权限的核心逻辑其实不复杂:在需要的时候请求,在拒绝的时候处理。但很多开发者容易忽略一个关键点——用户可能点击“不再询问”。
标准的请求流程应该是这样的:
- 检查权限是否已授予
- 如果未授予,检查是否需要展示 rationale(解释为什么需要这个权限)
- 发起权限请求
- 处理回调结果
我在项目中遇到过最典型的坑:直接在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版发布时就做适配,等正式版出来时已经稳如老狗了。
最后,用一张图总结整个权限管理的知识体系:
权限管理看似简单,但细节决定成败。从请求流程的设计,到库的选择,再到不同版本的适配,每一步都值得认真对待。记住:尊重用户的选择权,提供清晰的解释,做好异常处理,你的App在权限这块就不会出大问题。
最后分享一个小技巧:在开发阶段,可以在Application的onCreate里临时授予所有权限,方便调试。但上线前一定要删掉这段代码,否则会被Google Play拒审。
公众号:蓝海资料掘金营,微信deep3321