摘要:Android 权限处理真正难的地方,不是调用一次授权 API,而是同时面对系统版本差异、用户拒绝、永久拒绝、设置页返回和业务降级。本文以拍照和选择图片为例,梳理运行时权限的完整流程,并用 Kotlin 封装一个可复用的权限状态模型,让页面能够明确处理授权、拒绝和“前往设置”三类结果。
很多页面只验证了“用户点击允许”这一条路径:声明权限、调用请求方法、拿到回调后继续业务。真实用户却可能拒绝权限、只允许本次使用、在系统设置中关闭权限,或者在低版本和高版本上看到完全不同的授权界面。
权限处理如果散落在 Activity、Fragment 和业务回调中,常见问题包括:
因此,权限应该被视为一个有状态的业务前置条件,而不是一个布尔值。
权限首先要写入 `AndroidManifest.xml`。以相机和通知为例:
<manifest ...>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application ... />
</manifest>Manifest 声明只是告诉系统应用可能使用某项能力,不代表用户已经授权。危险权限通常还需要在应用运行过程中向用户申请。
权限的处理可以按下面的维度判断:
维度 | 需要关注的内容
权限组 | 相机、定位、麦克风、通知、媒体等能力分别处理
系统版本 | 新版本可能拆分权限,旧版本可能使用不同权限名
使用时机 | 尽量在用户触发具体功能时申请,而不是启动就弹窗
拒绝策略 | 区分暂时拒绝、永久拒绝和系统不允许的情况
替代路径 | 没有权限时是否仍能完成部分任务
现代项目建议使用 `ActivityResultContracts`,让请求和回调保持在同一个组件边界内。单个权限的最小实现如下:
class CameraActivity : AppCompatActivity() {
private val requestCamera = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
openCamera()
} else {
showCameraPermissionDenied()
}
}
private fun onTakePhotoClick() {
when {
ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED -> openCamera()
else -> requestCamera.launch(Manifest.permission.CAMERA)
}
}
}这里有三个关键点:
1. Launcher 在初始化阶段注册,点击事件只负责检查状态或调用 `launch`。
2. 已授权时直接进入相机,避免每次都弹授权框。
3. 回调只表达本次请求结果,后续如何降级由页面或 ViewModel 决定。
多个权限可以使用 `RequestMultiplePermissions`:
private val requestMedia = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { result ->
val imageGranted = result[Manifest.permission.READ_MEDIA_IMAGES] == true
val videoGranted = result[Manifest.permission.READ_MEDIA_VIDEO] == true
if (imageGranted || videoGranted) {
openMediaPicker()
} else {
showMediaFallback()
}
}不要只看 `Map` 中是否存在 key。对于不同系统版本,应用真正需要的权限集合可能不同,应先根据版本构造请求列表。
权限 API 的兼容代码最好集中在一个类里,不要让每个页面都写一遍版本判断。
以读取图片为例,Android 13 及以上使用媒体细分权限,较低版本通常使用外部存储读取权限:
fun imageReadPermission(): String? {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU ->
Manifest.permission.READ_MEDIA_IMAGES
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ->
Manifest.permission.READ_EXTERNAL_STORAGE
else -> null
}
}如果 `imageReadPermission()` 返回 `null`,表示当前系统不需要运行时申请。调用方可以直接打开选择器:
fun chooseImage() {
val permission = imageReadPermission()
if (permission == null || hasPermission(permission)) {
openMediaPicker()
} else {
requestImagePermission.launch(permission)
}
}不过,能使用系统 Photo Picker 时,优先使用 Photo Picker 往往比申请整个媒体库的读取权限更合适。用户只授权当前选择的资源,应用也能减少不必要的数据访问范围。
单纯返回 `false` 不足以指导界面。页面至少需要知道:当前是否已授权、是否还能再次请求、是否应该引导用户进入设置。
可以先定义一个稳定的状态模型:
sealed interface PermissionState {
data object Granted : PermissionState
data object Requestable : PermissionState
data object PermanentlyDenied : PermissionState
}将系统 API 封装起来:
class PermissionChecker(private val activity: Activity) {
fun state(permission: String): PermissionState {
val granted = ContextCompat.checkSelfPermission(
activity,
permission
) == PackageManager.PERMISSION_GRANTED
if (granted) return PermissionState.Granted
val canAskAgain = ActivityCompat.shouldShowRequestPermissionRationale(
activity,
permission
)
return if (canAskAgain) {
PermissionState.Requestable
} else {
PermissionState.PermanentlyDenied
}
}
}这里要注意一个容易误解的地方:`shouldShowRequestPermissionRationale()` 返回 `false` 并不只代表用户永久拒绝,也可能代表用户从来没有拒绝过。因此,首次请求前不要直接把 `false` 当成“已永久拒绝”。实际项目可以用本地状态记录是否已经请求过,再与系统结果一起判断。
一个简单的判断流程是:
fun nextAction(
permission: String,
requestedBefore: Boolean
): PermissionState {
if (hasPermission(permission)) return PermissionState.Granted
val rationale = ActivityCompat.shouldShowRequestPermissionRationale(
activity,
permission
)
return when {
!requestedBefore -> PermissionState.Requestable
rationale -> PermissionState.Requestable
else -> PermissionState.PermanentlyDenied
}
}这个模型不是为了准确猜测系统内部状态,而是为了把页面决策固定下来:第一次请求直接申请;用户明确拒绝后解释原因并允许重试;系统不再弹窗时引导设置或使用替代功能。
权限被拒绝以后,页面应该给用户一个明确且可执行的选择,而不是只显示“权限不足”。以拍照为例,可以设计为:
private fun showCameraPermissionDenied() {
val state = checker.state(Manifest.permission.CAMERA)
when (state) {
PermissionState.Requestable -> {
showPermissionDialog(
message = "拍照功能需要使用相机权限",
positive = "再次授权",
onPositive = {
requestCamera.launch(Manifest.permission.CAMERA)
}
)
}
PermissionState.PermanentlyDenied -> {
showPermissionDialog(
message = "相机权限已关闭,请在系统设置中开启",
positive = "去设置",
onPositive = ::openAppSettings
)
}
PermissionState.Granted -> openCamera()
}
}跳转设置页时不要假设用户一定会开启权限。返回页面后重新检查:
private val settingsLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (hasPermission(Manifest.permission.CAMERA)) {
openCamera()
} else {
showCameraPermissionDenied()
}
}
private fun openAppSettings() {
val intent = Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.parse("package:$packageName")
)
settingsLauncher.launch(intent)
}权限说明弹窗最好只在用户已经触发功能之后出现。启动页面就申请相机、定位等权限,既会降低授权率,也让用户难以理解权限和当前操作的关系。
权限请求应该是业务动作的前置步骤,但不应让 ViewModel 持有 Activity 或 `ActivityResultLauncher`。可以由页面负责系统交互,再把结果转换成业务事件:
class ProfileViewModel : ViewModel() {
private val _events = MutableSharedFlow<ProfileEvent>()
val events = _events.asSharedFlow()
fun onAvatarSelected(uri: Uri) {
viewModelScope.launch {
_events.emit(ProfileEvent.UploadAvatar(uri))
}
}
}Fragment 只负责权限和选择器:
private val pickAvatar = registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri ->
uri ?: return@registerForActivityResult
viewModel.onAvatarSelected(uri)
}这样做有两个收益:ViewModel 可以在单元测试中直接验证业务行为,页面也可以替换不同的系统入口,而不需要修改上传逻辑。
Manifest 声明不会自动弹窗。对于危险权限,必须在合适的用户操作中发起请求,并处理回调。
已授权时不需要重复请求。先检查 `checkSelfPermission`,把已授权路径和未授权路径分开。
取消、拒绝、设置页返回、进程重建都属于正常路径。它们应该有对应的状态和界面反馈。
首次请求前 `shouldShowRequestPermissionRationale` 也可能返回 `false`。要结合本地“是否请求过”记录判断,不能只依赖一个系统方法。
优先考虑 Photo Picker、`GetContent` 等系统选择器。权限越少,隐私风险和审核解释成本越低。
权限请求需要可见的 Activity 或 Fragment 和明确的用户交互。WorkManager、Repository 等后台组件不应该直接弹权限框,应先由前台页面完成授权,再启动任务。
权限功能至少要覆盖以下场景:
自动化测试可以把系统权限交互和业务逻辑拆开:权限封装测试状态转换,ViewModel 测试授权后的业务事件,UI 测试验证拒绝后的文案和按钮行为。这样失败时能够快速判断是系统交互、状态判断还是业务处理的问题。
Android 权限的稳定实现,可以归纳为四条原则:
1. Manifest 声明、运行时申请和业务使用是三个不同阶段。
2. 权限请求应贴近用户触发的具体功能,并使用 Activity Result API 管理回调。
3. 版本差异和系统选择器优先级应该集中封装,页面不重复编写判断。
4. 拒绝不是异常,而是必须设计的状态;没有权限时要提供重试、设置和替代路径。
当权限从“弹一次窗”升级为可解释的状态模型后,页面行为会更稳定,用户也更清楚为什么需要授权以及下一步该怎么做。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。