3. 埋点生命周期状态枚举

埋点从产生到最终上报,会经历多个明确的状态阶段。将这些阶段抽象为枚举值,能够使状态机逻辑可读、可扩展,并方便调试与监控。Android 埋点状态机采用以下 8 个核心状态:

public enum TrackState {
    IDLE,           // 空闲:埋点对象刚创建,未开始采集
    READY,          // 就绪:数据已组装,等待发送
    COLLECTING,     // 采集中:数据正在填充或聚合
    UPLOADING,      // 上传中:网络请求已发出
    RETRYING,       // 重试中:上传失败后的指数退避重试
    COMPLETED,      // 已完成:服务器确认接收
    FAILED,         // 失败:超过最大重试次数或不可恢复错误
    CANCELLED       // 取消:因业务逻辑或用户操作主动中止
}

3.1 状态定义与职责

状态触发条件可转移至典型场景
IDLE构造埋点对象READY, CANCELLED预创建埋点,暂未开始采集
READY采集参数校验通过COLLECTING, CANCELLED埋点数据就绪,等待入队
COLLECTING进入队列消费循环UPLOADING, FAILED, CANCELLED追加公共属性、加密、压缩
UPLOADING网络请求开始COMPLETED, RETRYING, FAILEDHTTP 请求已发送
RETRYING收到可重试错误UPLOADING, FAILED网络超时、HTTP 500
COMPLETED服务器 200+ 成功响应(终态)埋点生命周期正常结束
FAILED不可恢复/达到重试上限(终态)参数错误、鉴权失败
CANCELLED调用 cancel() 或生命周期触发(终态)用户退出页面、埋点过期

3.2 状态流转规则

核心原则:单向流转,不允许回跳到更早状态;终态(COMPLETED、FAILED、CANCELLED)不可再转移。

状态转换由 TrackStateMachine 统一调度,避免外部直接修改。以下为合法转换表:

// 状态转换矩阵(简化Kotlin实现)
class TrackStateMachine(private val id: String) {
    var state: TrackState = TrackState.IDLE
        private set

    fun transitionTo(newState: TrackState): Boolean {
        val allowed = allowedTransitions[state]?.contains(newState) ?: false
        if (allowed) {
            state = newState
            Logger.d("$id: ${state} -> ${newState}")
            return true
        }
        Logger.w("$id: 非法转换 ${state} -> ${newState}")
        return false
    }

    companion object {
        private val allowedTransitions = mapOf(
            TrackState.IDLE to setOf(TrackState.READY, TrackState.CANCELLED),
            TrackState.READY to setOf(TrackState.COLLECTING, TrackState.CANCELLED),
            TrackState.COLLECTING to setOf(TrackState.UPLOADING, TrackState.FAILED, TrackState.CANCELLED),
            TrackState.UPLOADING to setOf(TrackState.COMPLETED, TrackState.RETRYING, TrackState.FAILED),
            TrackState.RETRYING to setOf(TrackState.UPLOADING, TrackState.FAILED),
            // 终态无出边
        )
    }
}

3.3 状态流转图

埋点生命周期状态流转图 IDLE READY COLLECTING UPLOADING RETRYING COMPLETED FAILED CANCELLED

3.4 实现流程与关键代码

每个状态对应一个处理器,由状态机根据当前状态分发事件。以下展示 COLLECTING 状态下的数据组装逻辑:

class CollectingHandler : StateHandler {
    override fun handle(event: TrackEvent): TrackState {
        when (event) {
            is CollectEvent -> {
                // 补充公共参数:时间戳、设备ID、SessionID
                event.bundle.putLong("_ts", System.currentTimeMillis())
                event.bundle.putString("_did", DeviceInfo.getId())
                // 数据压缩(可选)
                val compressed = Compressor.gzip(event.data)
                return TrackState.UPLOADING.copyWithPayload(compressed)
            }
            is CancelEvent -> return TrackState.CANCELLED
            is ErrorEvent -> return TrackState.FAILED
        }
    }
}

3.5 配置示例

状态机可通过 YAML 配置重试策略,运行时注入:

# track_state_machine_config.yaml
state:
  RETRYING:
    maxRetries: 3
    backoffBaseMs: 1000    # 指数退避基数
    backoffMultiplier: 2.0
  UPLOADING:
    timeoutMs: 10000
    useHttp2: true
  IDLE:
    autoTransitionToReadyMs: 500  # 空闲自动就绪超时

3.6 调试方法

  • 日志标签:统一使用 TrackStateMachine,通过 adb logcat 过滤特定 ID 的状态变化。
  • 状态快照:提供 dumpState() 方法输出所有活跃埋点的当前状态、停留时间、重试次数。
  • 单元测试验证:覆盖所有合法/非法转换边界。
// 调试快照示例
adb logcat -s TrackStateMachine
输出: [TrackStateMachine] event_1234: COLLECTING -> UPLOADING (took 12ms)
                event_1234: UPLOADING -> RETRYING (reason: timeout)
                event_1234: RETRYING -> UPLOADING (attempt 2/3)

3.7 常见错误与优化

常见错误:直接暴露状态字段给外部修改,导致状态不一致。例如 track.state = TrackState.COMPLETED 会绕过重试逻辑。
解决方案:状态字段设为 private set,强制走 transitionTo() 方法。
性能优化:
- 状态比较使用 when 表达式而非链式 if,编译器生成跳转表。
- 高频状态(READY、COLLECTING)使用对象池避免频繁分配。
- 终态埋点对象立即回收到对象池,减少 GC 压力。

3.8 部署注意事项

在 Android 多进程场景中(如推送进程与主进程),建议每个进程维护独立的状态机实例。跨进程传递埋点对象时,只传递序列化的 payload 和最终状态,避免状态竞争。可通过 @IntDef 注解替代枚举以节省内存:

@IntDef({IDLE, READY, COLLECTING, UPLOADING, RETRYING, COMPLETED, FAILED, CANCELLED})
@Retention(RetentionPolicy.SOURCE)
public @interface TrackStateDef {}

public static final int IDLE = 0;
public static final int READY = 1;  // ... 其余略

通过本章节的状态枚举设计与流转规则,您可以为后续的采集器、发送器、重试器等模块奠定严格的边界约束,避免随意跳转导致的埋点数据丢失或重复上报问题。