Skip to content

权限系统

源:Android Permissions | iOS Permissions

权限管理是跨平台开发中最复杂的差异之一。Android 采用运行时权限请求,iOS 依赖 Info.plist 声明与系统弹窗,Desktop 则几乎没有权限限制。本文提供统一的权限请求抽象。

平台差异对比

平台权限模型请求时机用户体验拒绝后重新请求
Android运行时请求(API 23+)首次使用时系统弹窗可重新请求(拒绝 2 次后需跳转设置)
iOS首次请求弹窗首次使用时系统弹窗拒绝后只能跳转设置
Desktop无(操作系统级)---

常见权限类型对比

权限类型AndroidiOSDesktop
相机CAMERANSCameraUsageDescription无需权限
相册READ_MEDIA_IMAGES (API 33+)NSPhotoLibraryUsageDescription无需权限
位置ACCESS_FINE_LOCATIONNSLocationWhenInUseUsageDescription无需权限
麦克风RECORD_AUDIONSMicrophoneUsageDescription操作系统级
通知POST_NOTIFICATIONS (API 33+)系统自动请求无需权限
存储READ_EXTERNAL_STORAGE (API < 33)自动授权(沙盒内)无需权限

expect/actual 实现方案

API 核心签名说明

  • enum class Permission
  • enum class PermissionStatus
  • expect class PermissionManager
  • suspend fun PermissionManager.checkPermission(permission: Permission): PermissionStatus
  • suspend fun PermissionManager.requestPermission(permission: Permission): PermissionStatus
  • fun 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 侧权限已变更
}