权限系统
源:Android Permissions | iOS Permissions
权限管理是跨平台开发中最复杂的差异之一。Android 采用运行时权限请求,iOS 依赖 Info.plist 声明与系统弹窗,Desktop 则几乎没有权限限制。本文提供统一的权限请求抽象。
平台差异对比
| 平台 | 权限模型 | 请求时机 | 用户体验 | 拒绝后重新请求 |
|---|---|---|---|---|
| Android | 运行时请求(API 23+) | 首次使用时 | 系统弹窗 | 可重新请求(拒绝 2 次后需跳转设置) |
| iOS | 首次请求弹窗 | 首次使用时 | 系统弹窗 | 拒绝后只能跳转设置 |
| Desktop | 无(操作系统级) | - | - | - |
常见权限类型对比
| 权限类型 | Android | iOS | Desktop |
|---|---|---|---|
| 相机 | CAMERA | NSCameraUsageDescription | 无需权限 |
| 相册 | READ_MEDIA_IMAGES (API 33+) | NSPhotoLibraryUsageDescription | 无需权限 |
| 位置 | ACCESS_FINE_LOCATION | NSLocationWhenInUseUsageDescription | 无需权限 |
| 麦克风 | RECORD_AUDIO | NSMicrophoneUsageDescription | 操作系统级 |
| 通知 | POST_NOTIFICATIONS (API 33+) | 系统自动请求 | 无需权限 |
| 存储 | READ_EXTERNAL_STORAGE (API < 33) | 自动授权(沙盒内) | 无需权限 |
expect/actual 实现方案
API 核心签名说明
enum class Permissionenum class PermissionStatusexpect class PermissionManagersuspend fun PermissionManager.checkPermission(permission: Permission): PermissionStatussuspend fun PermissionManager.requestPermission(permission: Permission): PermissionStatusfun PermissionManager.openAppSettings()
标准代码块
kotlin
enum class Permission {
CAMERA,
PHOTO_LIBRARY,
LOCATION,
MICROPHONE,
NOTIFICATIONS,
}
enum class PermissionStatus {
GRANTED, // 已授予
DENIED, // 拒绝
DENIED_FOREVER, // 永久拒绝(需跳转设置)
NOT_DETERMINED // 未询问(iOS)
}
expect class PermissionManager {
suspend fun checkPermission(permission: Permission): PermissionStatus
suspend fun requestPermission(permission: Permission): PermissionStatus
fun openAppSettings()
}
// 业务层使用
class CameraFeature(private val permissionManager: PermissionManager) {
suspend fun openCamera(): Boolean {
val status = permissionManager.checkPermission(Permission.CAMERA)
return when (status) {
PermissionStatus.GRANTED -> {
// 直接打开相机
true
}
PermissionStatus.NOT_DETERMINED, PermissionStatus.DENIED -> {
// 请求权限
val result = permissionManager.requestPermission(Permission.CAMERA)
result == PermissionStatus.GRANTED
}
PermissionStatus.DENIED_FOREVER -> {
// 提示用户跳转设置
showPermissionDeniedDialog()
false
}
}
}
private fun showPermissionDeniedDialog() {
// 显示对话框,引导用户跳转设置
}
}kotlin
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
actual class PermissionManager(private val activity: ComponentActivity) {
private val permissionMap = mapOf(
Permission.CAMERA to Manifest.permission.CAMERA,
Permission.PHOTO_LIBRARY to if (android.os.Build.VERSION.SDK_INT >= 33) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
},
Permission.LOCATION to Manifest.permission.ACCESS_FINE_LOCATION,
Permission.MICROPHONE to Manifest.permission.RECORD_AUDIO,
Permission.NOTIFICATIONS to if (android.os.Build.VERSION.SDK_INT >= 33) {
Manifest.permission.POST_NOTIFICATIONS
} else {
null // Android 12 及以下无需请求通知权限
}
)
actual suspend fun checkPermission(permission: Permission): PermissionStatus {
val androidPermission = permissionMap[permission] ?: return PermissionStatus.GRANTED
return when {
ContextCompat.checkSelfPermission(activity, androidPermission) ==
PackageManager.PERMISSION_GRANTED -> {
PermissionStatus.GRANTED
}
activity.shouldShowRequestPermissionRationale(androidPermission) -> {
PermissionStatus.DENIED
}
else -> {
// 首次请求或永久拒绝
val prefs = activity.getSharedPreferences("permissions", Context.MODE_PRIVATE)
val hasAsked = prefs.getBoolean(androidPermission, false)
if (hasAsked) {
PermissionStatus.DENIED_FOREVER
} else {
PermissionStatus.NOT_DETERMINED
}
}
}
}
actual suspend fun requestPermission(permission: Permission): PermissionStatus {
val androidPermission = permissionMap[permission] ?: return PermissionStatus.GRANTED
// 记录已请求过
val prefs = activity.getSharedPreferences("permissions", Context.MODE_PRIVATE)
prefs.edit().putBoolean(androidPermission, true).apply()
return suspendCancellableCoroutine { continuation ->
val launcher = activity.registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
val status = if (isGranted) {
PermissionStatus.GRANTED
} else if (activity.shouldShowRequestPermissionRationale(androidPermission)) {
PermissionStatus.DENIED
} else {
PermissionStatus.DENIED_FOREVER
}
continuation.resume(status)
}
launcher.launch(androidPermission)
}
}
actual fun openAppSettings() {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", activity.packageName, null)
}
activity.startActivity(intent)
}
}kotlin
import platform.AVFoundation.*
import platform.Photos.*
import platform.CoreLocation.*
import platform.UIKit.UIApplication
import platform.Foundation.NSURL
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
actual class PermissionManager {
actual suspend fun checkPermission(permission: Permission): PermissionStatus {
return when (permission) {
Permission.CAMERA -> checkCameraPermission()
Permission.PHOTO_LIBRARY -> checkPhotoLibraryPermission()
Permission.LOCATION -> checkLocationPermission()
Permission.MICROPHONE -> checkMicrophonePermission()
Permission.NOTIFICATIONS -> PermissionStatus.GRANTED // iOS 自动处理
}
}
actual suspend fun requestPermission(permission: Permission): PermissionStatus {
return when (permission) {
Permission.CAMERA -> requestCameraPermission()
Permission.PHOTO_LIBRARY -> requestPhotoLibraryPermission()
Permission.LOCATION -> requestLocationPermission()
Permission.MICROPHONE -> requestMicrophonePermission()
Permission.NOTIFICATIONS -> PermissionStatus.GRANTED
}
}
actual fun openAppSettings() {
val settingsUrl = NSURL.URLWithString("app-settings:")
if (UIApplication.sharedApplication.canOpenURL(settingsUrl!!)) {
UIApplication.sharedApplication.openURL(settingsUrl)
}
}
private fun checkCameraPermission(): PermissionStatus {
return when (AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo)) {
AVAuthorizationStatusAuthorized -> PermissionStatus.GRANTED
AVAuthorizationStatusDenied, AVAuthorizationStatusRestricted -> PermissionStatus.DENIED_FOREVER
AVAuthorizationStatusNotDetermined -> PermissionStatus.NOT_DETERMINED
else -> PermissionStatus.NOT_DETERMINED
}
}
private suspend fun requestCameraPermission(): PermissionStatus = suspendCancellableCoroutine { continuation ->
AVCaptureDevice.requestAccessForMediaType(AVMediaTypeVideo) { granted ->
continuation.resume(
if (granted) PermissionStatus.GRANTED else PermissionStatus.DENIED_FOREVER
)
}
}
private fun checkPhotoLibraryPermission(): PermissionStatus {
return when (PHPhotoLibrary.authorizationStatus()) {
PHAuthorizationStatusAuthorized, PHAuthorizationStatusLimited -> PermissionStatus.GRANTED
PHAuthorizationStatusDenied, PHAuthorizationStatusRestricted -> PermissionStatus.DENIED_FOREVER
PHAuthorizationStatusNotDetermined -> PermissionStatus.NOT_DETERMINED
else -> PermissionStatus.NOT_DETERMINED
}
}
private suspend fun requestPhotoLibraryPermission(): PermissionStatus = suspendCancellableCoroutine { continuation ->
PHPhotoLibrary.requestAuthorization { status ->
val result = when (status) {
PHAuthorizationStatusAuthorized, PHAuthorizationStatusLimited -> PermissionStatus.GRANTED
PHAuthorizationStatusDenied, PHAuthorizationStatusRestricted -> PermissionStatus.DENIED_FOREVER
else -> PermissionStatus.NOT_DETERMINED
}
continuation.resume(result)
}
}
// 位置权限需要创建 CLLocationManager 实例
private fun checkLocationPermission(): PermissionStatus {
// 简化示例
return PermissionStatus.NOT_DETERMINED
}
private suspend fun requestLocationPermission(): PermissionStatus {
// 需要通过 CLLocationManagerDelegate 回调
return PermissionStatus.NOT_DETERMINED
}
private fun checkMicrophonePermission(): PermissionStatus {
return when (AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeAudio)) {
AVAuthorizationStatusAuthorized -> PermissionStatus.GRANTED
AVAuthorizationStatusDenied, AVAuthorizationStatusRestricted -> PermissionStatus.DENIED_FOREVER
AVAuthorizationStatusNotDetermined -> PermissionStatus.NOT_DETERMINED
else -> PermissionStatus.NOT_DETERMINED
}
}
private suspend fun requestMicrophonePermission(): PermissionStatus = suspendCancellableCoroutine { continuation ->
AVCaptureDevice.requestAccessForMediaType(AVMediaTypeAudio) { granted ->
continuation.resume(
if (granted) PermissionStatus.GRANTED else PermissionStatus.DENIED_FOREVER
)
}
}
}kotlin
actual class PermissionManager {
// Desktop 平台无需权限管理
actual suspend fun checkPermission(permission: Permission): PermissionStatus {
return PermissionStatus.GRANTED
}
actual suspend fun requestPermission(permission: Permission): PermissionStatus {
return PermissionStatus.GRANTED
}
actual fun openAppSettings() {
// Desktop 无设置页面
}
}代码封装示例
以下是带用户友好提示和重试逻辑的完整封装:
kotlin
// commonMain
class PermissionHandler(
private val manager: PermissionManager
) {
suspend fun requestWithRationale(
permission: Permission,
rationale: String,
onShowRationale: (String, () -> Unit) -> Unit,
onDeniedForever: () -> Unit
): Boolean {
val status = manager.checkPermission(permission)
return when (status) {
PermissionStatus.GRANTED -> true
PermissionStatus.NOT_DETERMINED -> {
// 首次请求,直接请求权限
manager.requestPermission(permission) == PermissionStatus.GRANTED
}
PermissionStatus.DENIED -> {
// 曾拒绝,先显示说明
var result = false
onShowRationale(rationale) {
// 用户点击"继续"后请求
result = runBlocking {
manager.requestPermission(permission) == PermissionStatus.GRANTED
}
}
result
}
PermissionStatus.DENIED_FOREVER -> {
// 永久拒绝,引导跳转设置
onDeniedForever()
false
}
}
}
fun openSettings() {
manager.openAppSettings()
}
}第三方库推荐
Moko Permissions 推荐
IceRock 开发的成熟权限管理库,支持全平台。
优势:
- ✅ 统一 API,简化权限请求流程
- ✅ 支持权限组批量请求
- ✅ 自动处理平台差异
- ✅ 提供 Compose 扩展
使用示例:
kotlin
// commonMain
val permissionsController = PermissionsController()
suspend fun requestCameraPermission() {
permissionsController.providePermission(Permission.CAMERA)
}依赖补充
Android Activity Result API(自实现需要)
kotlin
kotlin {
sourceSets {
androidMain.dependencies {
implementation("androidx.activity:activity-ktx:1.8.2")
}
}
}Moko Permissions(推荐)
kotlin
kotlin {
sourceSets {
commonMain.dependencies {
implementation("dev.icerock.moko:permissions:0.17.0")
// Compose 扩展(可选)
implementation("dev.icerock.moko:permissions-compose:0.17.0")
}
}
}groovy
kotlin {
sourceSets {
commonMain {
dependencies{
implementation "dev.icerock.moko:permissions:0.17.0"
}
}
}
}toml
[versions]
moko-permissions = "0.17.0"
[libraries]
moko-permissions = { module = "dev.icerock.moko:permissions", version.ref = "moko-permissions" }最新稳定版本查看链接:https://github.com/icerockdev/moko-permissions/releases
iOS Info.plist 配置(必需)
在 iosApp/Info.plist 中添加权限描述:
xml
<key>NSCameraUsageDescription</key>
<string>需要访问相机以拍摄照片</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册以选择照片</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>需要获取位置以提供定位服务</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要访问麦克风以录制音频</string>实战坑点
Android 永久拒绝判断不准确
shouldShowRequestPermissionRationale 局限
该方法无法区分"首次请求"和"永久拒绝",需要自行记录状态。
解决方案:
使用 SharedPreferences 记录是否请求过:
kotlin
val hasAskedBefore = prefs.getBoolean("permission_$androidPermission", false)
prefs.edit().putBoolean("permission_$androidPermission", true).apply()iOS Info.plist 缺失导致闪退
未声明权限描述
iOS 应用调用敏感权限但未在 Info.plist 声明会直接崩溃。
解决方案:
开发阶段在日志中检查是否缺失声明,添加通用描述:
xml
<key>NSCameraUsageDescription</key>
<string>应用需要使用相机功能</string>Android 13+ 通知权限
Breaking Change
Android 13 (API 33) 引入 POST_NOTIFICATIONS 权限,旧版本无需请求。
解决方案:
kotlin
val notificationPermission = if (Build.VERSION.SDK_INT >= 33) {
Manifest.permission.POST_NOTIFICATIONS
} else {
null // 低版本返回 null 表示无需权限
}权限组废弃警告
::: caution 权限组不再保证 Android 11+ 同组权限不再自动授予,需单独请求每个权限。 :::
正确做法:
kotlin
// ❌ 错误:依赖权限组
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE))
// ✅ 正确:明确请求所需权限
requestPermissions(arrayOf(
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VIDEO
))iOS 位置权限降级
权限降级
用户可能先授予"始终允许",后来改为"使用期间",需处理降级场景。
监听权限变更:
swift
// Swift 侧实现 CLLocationManagerDelegate
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
// 通知 Kotlin 侧权限已变更
}