Coroutine权限请求生命周期管理指南 - 第4节

4.1 回调驱动的权限模型与协程的鸿沟

Android 原生权限请求(requestPermissions / Activity Result API)均采用回调/监听器模式。协程天然以顺序挂起编写异步逻辑,两种模型存在结构差异。直接使用回调会导致回调地狱、生命周期泄漏和测试困难。本章将讲解如何将权限请求封装为挂起函数(suspend fun),把回调转发为协程结果,并定义清晰的结果类型。

特性回调模式协程挂起函数
控制流嵌套回调顺序代码
异常传递手动 try-catch 包裹自动传播
生命周期需手动注销自动跟随协程作用域
重试需额外状态机使用 retry 操作符

4.2 核心原理:suspendCoroutine 与 suspendCancellableCoroutine

Kotlin 协程提供两个底层 API 将基于回调的 API 转译为挂起函数:

  • suspendCoroutine:挂起当前协程并提供一个 Continuation,通过调用 resume(value) 或 resumeWithException(e) 恢复协程。
  • suspendCancellableCoroutine:在挂起基础上增加可取消能力。当协程被取消时,可自动取消底层的权限请求(例如移除回调)并抛出 CancellationException,防止内存泄漏。
最佳实践:权限请求应当始终使用 suspendCancellableCoroutine,因为在用户离开页面时协程作用域会被取消,此时应撤销未完成的回调注册,避免 “已销毁 Activity 仍收到回调” 的异常。

4.3 结果类型定义

为防止魔法字符串和布尔值歧义,定义密封类(sealed class)作为权限请求结果:

sealed class PermissionResult {
    data class Granted(val permission: String) : PermissionResult()
    data class Denied(val permission: String, val shouldShowRationale: Boolean) : PermissionResult()
    data class PermanentlyDenied(val permission: String) : PermissionResult()
    data class Cancelled(val permission: String) : PermissionResult()
}

字段说明:

  • shouldShowRationale:表示是否可再次弹出解释对话框(仅 Denied 时有效)。
  • PermanentlyDenied:用户勾选 “不再询问”,需引导用户前往设置。
  • Cancelled:系统或用户主动取消请求。

4.4 实现流程:回调转协程

权限请求挂起函数实现流程 调用方(Activity/Fragment) suspendCancellableCoroutine 注册 ActivityResultLauncher 启动权限 intent 回调触发(onActivityResult) resume(PermissionResult) 调用方获得结果 重试决策 (retry 操作符实现)

完整代码实现

以下示例使用 Activity Result API + suspendCancellableCoroutine 封装单个权限请求:

class PermissionRequester(private val activity: FragmentActivity) {
    private val launcher = activity.activityResultRegistry.register(
        "permission_launcher",
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        // 回调由 resume 接管
    }

    suspend fun request(permission: String, scope: LifecycleCoroutineScope): PermissionResult {
        return suspendCancellableCoroutine { continuation ->
            // 注册回调为一次性
            val callback = object : ActivityResultCallback<Boolean> {
                override fun onActivityResult(result: Boolean?) {
                    if (result == true) {
                        continuation.resume(PermissionResult.Granted(permission))
                    } else {
                        val shouldShow = ActivityCompat.shouldShowRequestPermissionRationale(
                            activity, permission
                        )
                        val resultType = if (shouldShow) {
                            PermissionResult.Denied(permission, true)
                        } else {
                            PermissionResult.PermanentlyDenied(permission)
                        }
                        continuation.resume(resultType)
                    }
                }
            }
            // 启动请求
            launcher.launch(permission, activity, callback)

            // 取消时自动移除回调
            continuation.invokeOnCancellation {
                // 移除注册的回调(简化:通过 launcher.unregister)
                launcher.unregister()
            }
        }
    }
}

4.5 生命周期绑定与作用域选择

挂起函数必须在协程作用域内调用。权限请求通常与 UI 生命周期绑定:

  • ViewModel + viewModelScope:当 Activity 重建(旋转)时,请求结果仍保留在 ViewModel 中,无需重新请求。
  • Lifecycle.repeatOnLifecycle:在 Fragment 中,使用 viewLifecycleOwner.lifecycleScope 启动协程,并绑定到 STARTED 状态。
// 在 Fragment 中
lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        val result = permissionRequester.request(Manifest.permission.CAMERA, this)
        when (result) {
            is PermissionResult.Granted -> openCamera()
            is PermissionResult.Denied -> showRationale()
            is PermissionResult.PermanentlyDenied -> openSettings()
            is PermissionResult.Cancelled -> log("User cancelled")
        }
    }
}

4.6 异常处理与重试策略

挂起函数内部可能抛出 CancellationException(协程取消时)或 RuntimeException(例如 Activity 已销毁)。建议在调用侧使用 try-catch 包裹,并结合 retry 操作符实现重试:

lifecycleScope.launch(Dispatchers.Main) {
    val deferred = async {
        // 最多重试 2 次,间隔 300ms
        retry(2) {
            if (currentCoroutineContext().isActive) {
                permissionRequester.request(Manifest.permission.CAMERA, this@launch)
            } else throw CancellationException("Cancelled")
        }
    }
    try {
        val result = deferred.await()
        handleResult(result)
    } catch (e: CancellationException) {
        // 正常取消,不处理
    } catch (e: Exception) {
        // 记录异常,或 fallback 到默认行为
        Log.e("Perm", "Request failed", e)
    }
}

4.7 常见错误与调试

  • 忘记处理 CancellationException:直接调用 resumeWithException(e) 会导致协程非正常取消。始终检查 isActive。
  • 多次 resume:回调可能被多次触发(如系统复活的特殊情况)。在 suspendCancellableCoroutine 中需确保只调用一次 resume,建议使用 AtomicBoolean 或 continuation.isActive 保护。
  • Lifecycle 泄漏:若在 Lifecycle.State.DESTROYED 状态下仍持有回调,会抛出异常。始终通过 invokeOnCancellation 清理资源。
调试技巧:在挂起函数入口和回调处添加日志,打印当前线程和协程状态。使用 Android Studio 的 Coroutine Debugger 观察挂起/恢复行为。

4.8 性能与稳定性优化

问题优化方式
多次重试导致 UI 闪烁限制并发请求数量,使用 Mutex 保证同一权限不会同时发起多个请求
重复注册回调使用 weakReference 包裹 Activity,防止内存泄漏
取消时异步清理在 invokeOnCancellation 中移除所有注册,避免回调在销毁后触发
低端设备启动慢权限请求本身开销小,但若需要弹设置页,使用 withContext(Dispatchers.Main) 确保 UI 操作在主线程

通过本章封装,后续所有权限请求均可像调用普通函数一样顺序编写,自动处理生命周期与取消,并实现优雅的重试策略。这是构建健壮权限体系的基础步骤。