【音视频】iOS + Android 摄像头权限处理

一个权限请求,iOS 要写 40 行,Android 要写 50 行,还要处理”不再询问”的各种边界情况。我们用 Claude Code 一次性生成双端统一封装,以后新项目直接复用。

1、你写过多少遍权限代码?

做音视频开发,每个 App 都逃不过:

// iOS - 第 N 次写这段
AVCaptureDevice.requestAccess(for: .video) { granted in
    DispatchQueue.main.async {
        if granted { /* 打开相机 */ }
        else { /* 提示去设置 */ }
    }
}
// Android - 第 N 次写这段
val permissionLauncher = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { granted ->
    if (granted) { /* 打开相机 */ }
    else { /* 判断是否勾了"不再询问" */ }
}

痛点总结:

  1. API 差异大——iOS 用闭包回调,Android 用 ActivityResultLauncher
  2. 边界情况多——「拒绝」「不再询问」「系统设置中关闭」三种状态,各自处理
  3. 权限状态要同步——进入页面要检查当前状态,从设置回来要重新检查
  4. 每个项目都重写——样板代码复制粘贴,容易遗漏

2、 技术背景:双端权限模型对比

┌─────────────────────────────────────────────────────────┐
│                                                         │
│  iOS 权限模型                    Android 权限模型        │
│  ────────────                    ────────────────        │
│  请求 → 用户选择                 请求 → 用户选择          │
│    │                                │                   │
│    ├─ 允许 → 正常使用              ├─ 允许 → 正常使用     │
│    ├─ 拒绝 → 引导去设置            ├─ 拒绝 → 可再次请求   │
│    └─ (无"不再询问"概念)           └─ 拒绝+不再询问 →     │
│                                      只能引导去设置       │
│                                                         │
│  iOS 检查方式:                     Android 检查方式:      │
│  AVCaptureDevice                  ContextCompat          │
│  .authorizationStatus             .checkSelfPermission   │
│                                                         │
└─────────────────────────────────────────────────────────┘
对比维度iOSAndroid
权限声明Info.plistAndroidManifest.xml
请求方式AVCaptureDevice.requestAccessActivityResultContracts.RequestPermission
状态检查.authorizationStatusContextCompat.checkSelfPermission
「不再询问」无(iOS 无此概念)shouldShowRequestPermissionRationale
引导去设置UIApplicationOpenSettingsURLStringIntent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)

3、Claude Code 介入时刻

3.1、 我这样跟 Claude Code 说

帮我写一个 iOS 和 Android 双端统一的摄像头权限管理工具类。

要求:
1. iOS (Swift):封装 AVCaptureDevice 权限请求
   - 检查当前权限状态
   - 请求权限
   - 如果被拒绝,引导用户去系统设置
   - 从设置返回后自动重新检查

2. Android (Kotlin):封装 Manifest.permission.CAMERA 权限
   - 检查当前权限状态
   - 请求权限
   - 处理"不再询问"的情况
   - 引导用户去应用设置页面

3. 设计要求:
   - 双端接口尽量统一(相同的调用语义)
   - 支持 Compose / SwiftUI 的回调方式
   - 完整的错误处理和日志
   - 可以作为项目的基础工具类直接复用

4. 输出文件:
   - ios/CameraPermissionManager.swift
   - android/CameraPermissionManager.kt

3.2、 Claude Code 生成了什么(核心代码)

3.2.1、iOS 端 — CameraPermissionManager.swift

import AVFoundation
import UIKit
import Combine

// MARK: - 权限状态枚举(双端统一命名)
enum CameraPermissionStatus {
    case authorized       // 已授权,可以使用
    case denied           // 已拒绝(可再次请求)
    case restricted       // 受限制(家长控制等,仅 iOS)
    case notDetermined    // 尚未请求过
    case permanentlyDenied // 已拒绝且无法再次请求(仅 Android,iOS 保留兼容)
}

// MARK: - 权限管理器
finalclass CameraPermissionManager: ObservableObject {
    staticlet shared = CameraPermissionManager()

    @Publishedvar status: CameraPermissionStatus = .notDetermined

    privateinit() {
        refreshStatus()
    }

    // MARK: - 检查当前权限状态
    func refreshStatus() {
        let avStatus = AVCaptureDevice.authorizationStatus(for: .video)
        DispatchQueue.main.async {
            self.status = self.mapStatus(avStatus)
        }
    }

    // MARK: - 请求权限
    func requestPermission() async -> CameraPermissionStatus {
        let currentStatus = AVCaptureDevice.authorizationStatus(for: .video)

        switch currentStatus {
        case .authorized:
            return .authorized

        case .notDetermined:
            // 首次请求——系统弹窗
            let granted = await AVCaptureDevice.requestAccess(for: .video)
            let newStatus: CameraPermissionStatus = granted ? .authorized : .denied
            await MainActor.run { self.status = newStatus }
            return newStatus

        case .denied:
            // 已拒绝——引导去设置
            await MainActor.run { self.status = .denied }
            openSettings()
            return .denied

        case .restricted:
            await MainActor.run { self.status = .restricted }
            return .restricted

        @unknown default:
            return .notDetermined
        }
    }

    // MARK: - 引导用户去系统设置
    func openSettings() {
        guardlet url = URL(string: UIApplication.openSettingsURLString),
              UIApplication.shared.canOpenURL(url) else {
            print("[CameraPermission] 无法打开设置")
            return
        }
        UIApplication.shared.open(url)
    }

    // MARK: - 监听从设置返回(App 回到前台时重新检查)
    func startObservingForeground() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(willEnterForeground),
            name: UIApplication.willEnterForegroundNotification,
            object: nil
        )
    }

    @objcprivatefunc willEnterForeground() {
        refreshStatus()
        print("[CameraPermission] 从后台返回,当前状态: \(status)")
    }

    // MARK: - 状态映射
    privatefunc mapStatus(_ avStatus: AVAuthorizationStatus) -> CameraPermissionStatus {
        switch avStatus {
        case .authorized:    return .authorized
        case .denied:        return .denied
        case .restricted:    return .restricted
        case .notDetermined: return .notDetermined
        @unknown default:    return .notDetermined
        }
    }
}

3.2.2、Android 端 — CameraPermissionManager.kt

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.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

// MARK: - 权限状态枚举(与 iOS 对齐)
enumclass CameraPermissionStatus {
    AUTHORIZED,          // 已授权
    DENIED,              // 已拒绝(可再次请求)
    RESTRICTED,          // 受限制(仅 iOS,Android 保留兼容)
    NOT_DETERMINED,      // 尚未请求过
    PERMANENTLY_DENIED   // 用户勾选「不再询问」
}

// MARK: - 权限管理器
class CameraPermissionManager private constructor(
    privateval activity: ComponentActivity
) : DefaultLifecycleObserver {

    companionobject {
        privateconstval TAG = "CameraPermission"

        @Volatile
        privatevar instance: CameraPermissionManager? = null

        fun init(activity: ComponentActivity): CameraPermissionManager {
            return instance ?: synchronized(this) {
                instance ?: CameraPermissionManager(activity).also {
                    instance = it
                    activity.lifecycle.addObserver(it)
                }
            }
        }

        fun getInstance(): CameraPermissionManager {
            return instance ?: throw IllegalStateException(
                "CameraPermissionManager 未初始化,请先在 Activity.onCreate 中调用 init()"
            )
        }
    }

    // 权限状态流(Compose 可收集)
    privateval _status = MutableStateFlow(CameraPermissionStatus.NOT_DETERMINED)
    val status: StateFlow<CameraPermissionStatus> = _status.asStateFlow()

    // 权限请求 Launcher
    privatevar permissionLauncher: ActivityResultLauncher<String>? = null

    // 权限请求回调
    privatevar onResult: ((CameraPermissionStatus) -> Unit)? = null

    /**
     * 在 Activity.onCreate 中调用,注册权限 Launcher
     */

    fun register(activity: ComponentActivity) {
        permissionLauncher = activity.registerForActivityResult(
            ActivityResultContracts.RequestPermission()
        ) { granted ->
            val newStatus = if (granted) {
                CameraPermissionStatus.AUTHORIZED
            } else {
                // 判断用户是否勾选了「不再询问」
                if (activity.shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
                    CameraPermissionStatus.DENIED  // 仅拒绝,可再次请求
                } else {
                    CameraPermissionStatus.PERMANENTLY_DENIED  // 勾了不再询问
                }
            }
            _status.value = newStatus
            onResult?.invoke(newStatus)
            android.util.Log.d(TAG, "权限请求结果: $newStatus")
        }
    }

    /**
     * 检查当前权限状态
     */

    fun refreshStatus(): CameraPermissionStatus {
        val currentStatus = when {
            ContextCompat.checkSelfPermission(
                activity, Manifest.permission.CAMERA
            ) == PackageManager.PERMISSION_GRANTED -> CameraPermissionStatus.AUTHORIZED

            // 从未请求过
            activity.shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
                CameraPermissionStatus.NOT_DETERMINED
            }

            else -> {
                // 已拒绝 + 不再询问 → 永久拒绝
                // 已拒绝(首次)→ 仍可请求
                // 这里简化判断:如果没有授权且 shouldShowRationale 返回 false,
                // 说明要么是首次(NOT_DETERMINED),要么是永久拒绝
                CameraPermissionStatus.PERMANENTLY_DENIED
            }
        }
        _status.value = currentStatus
        return currentStatus
    }

    /**
     * 请求权限(挂起函数,供协程调用)
     */

    suspendfun requestPermission(): CameraPermissionStatus {
        val launcher = permissionLauncher
            ?: throw IllegalStateException("请先调用 register() 注册权限 Launcher")

        return kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
            onResult = { result ->
                continuation.resume(result) {}
            }
            launcher.launch(Manifest.permission.CAMERA)
        }
    }

    /**
     * 引导用户去应用设置页面
     */

    fun openAppSettings() {
        val intent = Intent(
            Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
            Uri.parse("package:${activity.packageName}")
        ).apply {
            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        }
        if (intent.resolveActivity(activity.packageManager) != null) {
            activity.startActivity(intent)
        } else {
            android.util.Log.e(TAG, "无法打开应用设置页面")
        }
    }

    overridefun onStart(owner: LifecycleOwner) {
        super.onStart(owner)
        // App 从后台返回时自动刷新权限状态
        refreshStatus()
    }

    overridefun onDestroy(owner: LifecycleOwner) {
        super.onDestroy(owner)
        owner.lifecycle.removeObserver(this)
    }
}

3.3、 我审查时改了哪里

AI 输出的问题我的修正为什么
Android 首次请求时 shouldShowRationale 返回 false 被误判为 PERMANENTLY_DENIED加了一层判断:先检查是否 NOT_DETERMINED首次安装时 shouldShowRationale 也返回 false
iOS 没有监听从设置页面返回加了 willEnterForeground Notification 监听用户从设置改了权限回来需要自动刷新
Android Manager 的 instance 在 Activity 重建时会丢失加了 synchronized + @Volatile + 生命周期观察配置变更(旋转屏幕)时保持单例

4、使用示例

4.1、iOS — SwiftUI

struct CameraView: View {
    @StateObjectprivatevar permissionManager = CameraPermissionManager.shared

    var body: some View {
        Group {
            switch permissionManager.status {
            case .authorized:
                CameraPreviewView()
            case .notDetermined:
                Button("开启摄像头") {
                    Task {
                        _ = await permissionManager.requestPermission()
                    }
                }
            case .denied:
                VStack {
                    Text("摄像头权限被拒绝")
                    Button("去设置") {
                        permissionManager.openSettings()
                    }
                }
            default:
                Text("摄像头不可用")
            }
        }
        .onAppear {
            permissionManager.refreshStatus()
            permissionManager.startObservingForeground()
        }
    }
}

4.2、Android — Jetpack Compose

@Composable
fun CameraScreen(activity: ComponentActivity) {
    val manager = remember { CameraPermissionManager.getInstance() }
    val status by manager.status.collectAsState()

    DisposableEffect(activity) {
        manager.register(activity)
        onDispose { }
    }

    when (status) {
        CameraPermissionStatus.AUTHORIZED -> {
            CameraPreview()
        }
        CameraPermissionStatus.NOT_DETERMINED -> {
            Button(onClick = {
                // 在协程中请求权限
                lifecycleScope.launch {
                    manager.requestPermission()
                }
            }) {
                Text("开启摄像头")
            }
        }
        CameraPermissionStatus.DENIED -> {
            Column {
                Text("摄像头权限被拒绝")
                Button(onClick = { manager.requestPermission() }) {
                    Text("再次请求")
                }
            }
        }
        CameraPermissionStatus.PERMANENTLY_DENIED -> {
            Column {
                Text("请在设置中开启摄像头权限")
                Button(onClick = { manager.openAppSettings() }) {
                    Text("去设置")
                }
            }
        }
        else -> {
            Text("摄像头不可用")
        }
    }
}

5、效果验证

指标传统方式(手写)Claude Code 生成
开发耗时~2 小时(iOS 40min + Android 60min + 联调 20min)~30 分钟(生成 5min + 审查修改 15min + 测试 10min)
代码行数iOS 80 行 + Android 100 行iOS 80 行 + Android 110 行(更健壮)
边界情况覆盖2/5(容易遗漏)5/5(AI 枚举了所有状态)
可复用性“下个项目再复制吧”直接复用

Claude Code 帮我省了 75% 的时间。最有价值的是它自动处理了「不再询问」、「从设置返回」等容易遗漏的边界情况。

学习和提升音视频开发技术,欢迎你加入我们的知识星球

【音视频】iOS + Android 摄像头权限处理

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐