第3章 状态定义与枚举:崩溃采集、等待上报、上报中、上报成功、上报失败、重试中、丢弃

崩溃上报链路的核心是状态机——一组有限的状态以及状态之间的转移规则。整个链路从崩溃发生到最终处理结果,共包含7个关键状态。本章逐一定义每个状态的语义、触发条件、典型行为,并以枚举形式固化为代码。

状态机设计原则:每个状态应具有唯一性、可观测性和可测试性。状态转移必须由明确的事件驱动,避免隐式跳转和循环依赖。

3.1 状态定义表

状态名称 枚举值(推荐) 语义 进入条件 典型行为
崩溃采集 COLLECTING 正在收集崩溃现场信息(堆栈、线程、日志、内存快照等) 原生崩溃信号 or Java异常被捕获 写入文件、序列化上下文
等待上报 WAITING 采集完成,等待网络条件/用户授权/定时器触发 崩溃采集完成 检查网络、弹窗确认(可选)
上报中 UPLOADING 正在向服务器发起HTTP/HTTPS请求 满足上报条件(网络可用、用户同意) 上传文件、监听进度
上报成功 SUCCESS 服务器返回200且校验通过 上报完成且无错误 清理本地缓存、标记已上报
上报失败 FAILED 服务器返回错误、超时或网络中断 请求失败或异常 记录错误原因、触发重试策略
重试中 RETRYING 正在等待指数退避重试间隔 上报失败且重试次数未耗尽 定时器、更新重试计数
丢弃 DISCARDED 放弃该条崩溃数据(不再处理) 重试次数耗尽/用户拒绝/采集超时/被动清理 删除文件、释放资源

3.2 状态枚举代码示例

在Android项目中,建议使用enum class定义状态,每个枚举值携带整数id用于日志和数据库存储:

public enum CrashReportState {
    /** 崩溃采集 */
    COLLECTING(1),
    /** 等待上报 */
    WAITING(2),
    /** 上报中 */
    UPLOADING(3),
    /** 上报成功 */
    SUCCESS(4),
    /** 上报失败 */
    FAILED(5),
    /** 重试中 */
    RETRYING(6),
    /** 丢弃 */
    DISCARDED(7);

    private final int id;

    CrashReportState(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }
}

3.3 状态转移图

下图展示了7个状态之间的标准转移路径。实线箭头表示正常流程,虚线箭头表示异常或超时路径。其中“重试中”状态会通过指数退避重新回到“上报中”,形成循环,直至达到最大重试次数后进入“丢弃”。

崩溃上报状态机——状态与转移 崩溃采集 等待上报 上报中 上报成功 上报失败 重试中 丢弃 采集完成 满足上报条件 成功 失败 触发重试 重试(指数退避) 重试耗尽 超时/用户拒绝 重要转移说明 • “崩溃采集”完成后必须进入“等待上报”,不可直接跳转“上报中”。 • “重试中”只能通过指数退避重新进入“上报中”或达到上限进入“丢弃”。

3.4 状态转移清单

  • 崩溃采集 → 等待上报:采集结束(成功或部分成功)后立即转移。
  • 等待上报 → 上报中:网络可用且满足上报策略(如WIFI限制、用户已授权)。
  • 等待上报 → 丢弃:等待超时(如30分钟)或用户通过界面选择“忽略”。
  • 上报中 → 上报成功:服务器返回2xx且body校验通过。
  • 上报中 → 上报失败:任何网络异常、服务器错误、超时或数据损坏。
  • 上报失败 → 重试中:已重试次数 < 最大重试次数(通常3次)。
  • 重试中 → 上报中:等待间隔(1s、2s、4s…)后触发。
  • 重试中 → 丢弃:已达最大重试次数或总生存时间超限。

3.5 重试策略与丢弃阈值

重试中状态使用指数退避 + 随机抖动以减轻服务器压力。常见配置:

initialDelay = 1000ms  // 首次重试前等待1秒
maxDelay = 30000ms     // 最大等待30秒
multiplier = 2.0       // 每次翻倍
jitter = 0.2           // 随机扰动 ±20%
maxRetryCount = 3      // 超过后转入丢弃
注意:丢弃状态是最终态,一旦进入不可再触发任何重试或上报。务必确保丢弃前所有相关资源(文件句柄、内存缓存)已正确释放,避免内存泄漏。

3.6 状态与线程安全

状态枚举本身是线程安全的(枚举实例不可变),但状态转换操作(如从 WAITING 转到 UPLOADING)必须使用原子操作或同步锁,防止并发上报导致重复请求。推荐使用 AtomicReference<CrashReportState> 或 StateMachine 框架。

import java.util.concurrent.atomic.AtomicReference;

public class CrashReportStateMachine {
    private final AtomicReference<CrashReportState> currentState =
            new AtomicReference<>(CrashReportState.COLLECTING);

    public boolean transitionTo(CrashReportState target) {
        // 这里仅演示原子更新,实际需校验合法性
        return currentState.compareAndSet(
                currentState.get(), target);
    }
}