3. 权限状态建模与请求流程设计

权限请求本质上是一个有限状态机(FSM)。在协程环境下,我们需要将权限的六种核心状态与四种触发动作建模为可重入、可取消的状态转换模型。本节定义状态枚举、绘制状态转换图,并实现基于协程的安全转换引擎。

3.1 权限状态枚举

每个权限在生命周期中都具有以下状态,使用 Kotlin sealed class 实现闭枚举。状态定义表如下:

状态名称 标志位 说明 是否可请求
NOT_REQUESTED 0 从未发起过请求 是
REQUESTING 1 请求已发出,等待用户决策 否
GRANTED 2 用户已授权 否(终态)
DENIED 3 用户拒绝,但可再次请求 是
PERMANENTLY_DENIED 4 用户勾选“不再询问”或系统拒绝 需引导设置
RETRYING 5 自动重试队列中,等待退避后重试 协程控制

此外,我们还定义 COMPLETED 作为请求流程完成的哨兵状态(内部使用)。枚举示例代码:

sealed class PermissionState {
    object NotRequested : PermissionState()
    object Requesting : PermissionState()
    object Granted : PermissionState()
    data class Denied(val shouldRetry: Boolean) : PermissionState()
    object PermanentlyDenied : PermissionState()
    data class Retrying(val attempt: Int, val delayMs: Long) : PermissionState()
}

3.2 请求状态转换图

下图展示了权限从 NotRequested 出发,经过请求、用户决策、重试等路径的完整状态转换。每个箭头代表一个动作或事件。

权限状态转换图 NotRequested Requesting Granted Denied PermanentlyDenied Retrying 请求 授权 拒绝 再次请求 不再询问 重试策略 退避后 说明:虚线表示可重复路径,实线表示一次触发。Granted 和 PermanentlyDenied 为终态。 Retrying 状态由协程定时器自动恢复至 Requesting。

3.3 协程状态机实现

使用 Channel 作为事件队列,flow 暴露状态变化,通过 stateIn 绑定到 CoroutineScope。核心状态循环:

class PermissionStateMachine(private val scope: CoroutineScope) {
    private val _state = MutableStateFlow<PermissionState>(PermissionState.NotRequested)
    val state: StateFlow<PermissionState> = _state.asStateFlow()

    private val events = Channel<PermissionAction>(Channel.UNLIMITED)

    sealed class PermissionAction {
        object Request : PermissionAction()
        object Grant : PermissionAction()
        object Deny : PermissionAction()
        data class PermanentDeny(val showSettings: Boolean) : PermissionAction()
        object Retry : PermissionAction()
    }

    init {
        scope.launch {
            events.consumeAsFlow().collect { action ->
                val current = _state.value
                _state.value = when {
                    // NotRequested → Requesting
                    current is PermissionState.NotRequested && action is PermissionAction.Request ->
                        PermissionState.Requesting
                    // Requesting → Granted
                    current is PermissionState.Requesting && action is PermissionAction.Grant ->
                        PermissionState.Granted
                    // Requesting → Denied
                    current is PermissionState.Requesting && action is PermissionAction.Deny ->
                        PermissionState.Denied(shouldRetry = true)
                    // Denied → Requesting (手动)
                    current is PermissionState.Denied && action is PermissionAction.Request ->
                        PermissionState.Requesting
                    // Denied → PermanentlyDenied
                    current is PermissionState.Denied && action is PermissionAction.PermanentDeny ->
                        PermissionState.PermanentlyDenied
                    // Denied → Retrying (自动)
                    current is PermissionState.Denied && action is PermissionAction.Retry ->
                        PermissionState.Retrying(attempt = 1, delayMs = 1000)
                    // Retrying → Requesting (退避后)
                    current is PermissionState.Retrying && action is PermissionAction.Request ->
                        PermissionState.Requesting
                    // 其他情况保持不变
                    else -> current
                }
            }
        }
    }

    fun send(action: PermissionAction) {
        events.trySend(action)
    }
}

3.4 异常处理与重试策略

生命周期安全:使用 viewModelScope 或 AndroidOwner.lifecycleScope 创建状态机实例,确保在 Fragment/Activity 重建时自动取消协程,避免内存泄漏。

重试策略采用指数退避 + 最大重试次数限制。请求失败时自动进入 Retrying 状态,并在延迟后发送 Retry 事件:

fun requestWithRetry(stateMachine: PermissionStateMachine, maxRetries: Int = 3) {
    stateMachine.send(PermissionAction.Request)
    // 监听状态变化,当进入 Denied 且 shouldRetry 为 true 时启动重试
    stateMachine.state.filterIsInstance<PermissionState.Denied>()
        .take(maxRetries)
        .onEach { deniedState ->
            val delay = (1L shl deniedState.hashCode()) * 500L // 指数退避:0.5s, 1s, 2s, ...
            delay(delay)
            stateMachine.send(PermissionAction.Retry)
        }
        .launchIn(scope)
}

3.5 常见错误与调试

  • 状态丢失:未使用 StateFlow 的重放机制,导致重建后状态重置。解决方案:在 ViewModel 中使用 stateIn(scope, SharingStarted.WhileSubscribed(5000), InitialValue) 保留状态。
  • 事件竞争(Race Condition):同时发送 Request 和用户回调的 Grant 导致状态跳跃。应在协程内部使用 mutex 或确保一次性消费。
  • 永久拒绝后仍重试:状态机未对 PermanentlyDenied 做守卫。在 requestWithRetry 入口处检查 _state.value 是否为 PermanentlyDenied,若是则直接跳转到 Settings 界面。
  • 协程取消后未清理事件通道:使用 Channel(UNLIMITED) 且未主动关闭,可能造成背压。推荐绑定生命周期并在 onCleared() 或 onStop() 时调用 events.close()。
性能注意:重试状态的退避时间建议使用 kotlinx.coroutines.delay 而非 Thread.sleep,避免阻塞调度线程。如果重试频繁,考虑使用 Flows.combine 合并多个权限状态,减少重复监听。