方案设计原则:可靠性、可重复性、隔离性、性能考量
做 Mock 回放方案设计,不是搭个玩具就完事。你在 Android 上做的每一次回放,都可能成为线上问题的“还原卡”。如果不可靠、不可重复、不隔离、性能还差,那这套方案还不如直接打 Log。
我个人习惯把这四个原则看作一个四边形的四个角。你稍微放松一点,整个形状就歪了。下面逐个拆解。
可靠性
可靠性是底线。你的回放如果时不时丢数据、行为对不上,那没人敢用。
我遇到过最典型的坑:WorkManager 的 Worker 被系统杀死后,回放记录跟着丢了。为什么?因为我把 Mock 数据存在内存里,没做持久化。后来改成每次回调都写本地文件,才算稳了。
- 强制持久化:所有 Mock 记录必须在 onStartJob 时写磁盘,不能依赖内存。
- 事务性写入:用 SQLite 或 MMKV 的事务保证一批数据要么全写要么全不写。
- 状态同步:每次 Worker 启动先检查本地是否有未回放的记录,有则优先处理。
还有一个细节:行为一致性。回放时不能因为延迟、线程切换等外部因素改变输出顺序。我习惯在每条记录里打上时间戳+序号,回放时严格按这个顺序执行。
可重复性
说白了,你跑到第 10 次回放,结果必须跟第 1 次一模一样。只有这样,你才能相信问题是可复现的,而不是玄学。
唯一标识是基础。每次 WorkManager 任务启动时,我给它生成一个全局唯一的 UUID,这个 UUID 贯穿整个回放链路。从录制的第一步到最后回放输出,所有日志、数据都用这个 ID 串联。
环境固定也很重要。回放时最好用固定的模拟器/真机镜像,时间、时区、语言、屏幕尺寸全部锁定。我曾经调试一个时区 bug,回放一直失败,排查两天才发现是因为回放机的时区被自动切换了。
| 因素 | 要求 | 常见问题 |
|---|---|---|
| 时间戳 | 统一以录制时的 UTC 为准 | 回放机时区偏移导致顺序错乱 |
| 线程调度 | 固定 WorkManager 的配置(如线程池大小) | 不同版本默认线程数不同 |
| 网络模拟 | Mock 掉所有网络请求,返回固定数据 | 真实网络波动导致结果不稳定 |
幂等性是可重复的保障。同一个任务被重试多次,结果应该相同。我建议在设计 Worker 的 onWork 方法时,先检查该 ID 的回放结果是否已经生成,如果有就直接返回,不再重复执行。
checksum 字段,回放开始时比对数据包完整性,不一致直接拒绝回放。
隔离性
你录制的 Mock 数据,不能影响正常的 WorkManager 调度。隔离不好,生产环境会出大乱子。
进程隔离是最彻底的。我见过有的团队把 Mock 回放做成独立进程,通过 AIDL 与主进程通信。好处是就算 Mock 崩了,完全不干扰宿主。缺点是麻烦,需要维护两套通信。
如果不想搞多进程,至少要做数据分域。所有 Mock 相关的数据库、SharedPreferences、文件目录,都加一个前缀,比如 mock_。我踩过坑:直接用默认的 SharedPreferences 存 Mock 配置,结果线上用户的某些设置被意外改掉。
另外,避免宿主副作用。Mock 回放不应该触发真实的推送、数据库写入、文件下载等。我的做法是:在 WorkManager 的 Configuration 里注入一个 MockExecutor,把 IO 操作全部重定向到临时目录,并且 hook 掉网络库。
性能考量
回放本身不能成为性能瓶颈。尤其是你需要在 CI 上跑大量回放用例的时候,性能差到跑不完,那就失去意义了。
第一个优化点:IO 优化。每次 WorkManager 回调都写入磁盘,次数多了会产生大量小文件。我改成批量写入,每条记录先累积到内存的 RingBuffer,攒够 50 条或超时 1 秒再 flush 到文件。这样 IO 次数减少 90% 以上。
第二个:增量回放。不需要每次都全量回放所有记录。打个比方,你测试同一个 Worker 在第 10 次调用时的行为,只需要播放前 10 条记录就够了。我设计了一个 SliceReplayer,可以指定起始-结束索引,按需回放。
// 增量回放示例
class SliceReplayer(private val records: List<MockRecord>) {
fun replay(fromIndex: Int, toIndex: Int) {
records.subList(fromIndex, toIndex).forEach { record ->
replayRecord(record)
}
}
private fun replayRecord(record: MockRecord) {
// 这里只回放指定的部分
}
}
第三个:懒加载。Mock 数据包可能很大(比如录了上百 MB 的网络流量)。如果一开始就全部加载到内存,App 直接 OOM。我改为使用 LazyRecordLoader,只加载当前 Worker 任务需要的那些记录,其余存在磁盘上。
- 单次回放启动时间:< 200ms(从 Worker 启动到第一条回放输出)
- 回放吞吐量:≥ 500 条/秒(在 Pixel 4 上测试)
- 内存峰值:< 50MB(回放 1 万条记录时)
达不到这些指标,建议先排查 IO 是不是太慢。
如何平衡四个原则?
你想想看,隔离性和性能天生就有点冲突。隔离得越彻底(多进程),通信成本越高,性能越差。可靠性和可重复性也有重叠的地方——都要求数据持久化,但持久化又影响性能。
我的经验是:先保可靠性,再优化性能。可靠性一旦出问题,用户信任度就没了。隔离性在初期可以适当放宽(比如用进程内分区代替多进程),但后期必须做到进程级别。可重复性则依赖于良好的设计文档和严格的 CI 环境。
- 你们团队能接受 Mock 回放偶尔丢失数据吗? → 不能 → 必须做事务+持久化(可靠性)
- 回放结果需要多人共享或跨设备复现吗? → 是 → 强制唯一标识+固定环境(可重复性)
- Mock 回放会和业务逻辑在同一个进程吗? → 前期可以接受,但上线前必须隔离(隔离性)
- CI 上的回放用例超过 2000 个了吗? → 是 → 开始做增量回放和懒加载(性能)
嗯,大致就是这样。这四个原则说起来简单,做起来每一步都有坑。但只要你把这四个框架搭好,后面的 Mock 数据格式、录制策略、回放调度都能沿着这条路走下去。我在实际项目中一直用这个四象限思维来评审方案,几乎没有出过大问题。