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, FAILED | HTTP 请求已发送 |
| 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 状态流转图
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() 方法。
性能优化:
- 状态比较使用
- 高频状态(READY、COLLECTING)使用对象池避免频繁分配。
- 终态埋点对象立即回收到对象池,减少 GC 压力。
- 状态比较使用
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; // ... 其余略
通过本章节的状态枚举设计与流转规则,您可以为后续的采集器、发送器、重试器等模块奠定严格的边界约束,避免随意跳转导致的埋点数据丢失或重复上报问题。