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