方案一:文件存储(JSON / Protobuf)——适用场景与架构边界
1. 方案边界定义
文件存储方案是最原始也最灵活的本地持久化手段。其核心思路是:将结构化或半结构化的数据对象,序列化为字节流写入磁盘文件,读取时再反序列化为内存对象。本方案聚焦于直接文件 I/O + 序列化协议这一组合,不依赖任何数据库引擎或 ORM 框架。
| 维度 | 边界说明 |
|---|---|
| 数据规模 | 单文件 ≤ 100 MB,总数据量 ≤ 1 GB。超过该阈值应评估 SQLite 或 LevelDB。 |
| 并发模型 | 仅支持单进程读写。多进程场景需额外实现文件锁(flock / LockFile)。 |
| 查询需求 | 无跨实体关联查询、无复杂过滤条件。仅支持全量加载后线性扫描。 |
| 事务语义 | 无 ACID 保证。如需原子写,必须手工实现 write-ahead + rename 模式。 |
| 使用场景举例 | App 配置缓存、用户偏好、离线草稿箱、轻量级埋点日志、模型本地快照。 |
架构决策点:文件存储不是“数据库”,它是一个序列化管道。架构师必须在方案选型阶段就明确它的边界——凡涉及事务、关联、范围查询、多进程并发四项能力中的任意一项,都需要立即淘汰本方案。
2. 分层结构与组件职责
整个文件存储方案在 App 架构中占据基础设施层(Infrastructure)的位置,向上为业务层提供仓储(Repository)接口。内部又分为三层:
- 序列化层(Serializer):负责对象 ↔ 字节流的编码/解码。JSON 使用反射或手动映射,Protobuf 使用预定义 .proto 生成的代码。
- 文件管理层(FileManager):封装文件路径、创建、写入、读取、删除操作;处理同名覆盖、目录自动创建、文件锁。
- 仓储适配层(Repository Adapter):将序列化 + 文件操作合并为业务可理解的接口,如
UserPreferencesRepository.save(key, obj)。
3. 组件职责矩阵
| 组件 | 职责 | 不允许做的事 |
|---|---|---|
| Serializer | 对单个实体进行 encode / decode,支持版本兼容(字段忽略/默认值) | 不处理文件路径、不管理缓存、不触发 I/O |
| FileManager | 文件路径拼接、目录创建、原子写(写临时 → rename)、文件锁 | 不涉及业务实体结构、不进行序列化/反序列化 |
| Repository | 组合序列化+文件操作,对外暴露 load()、save()、delete() | 不暴露底层文件句柄、不混合多种序列化协议 |
4. 数据流与控制流
以“保存用户偏好”为例,典型控制流如下:
- 业务层调用
UserPrefRepo.save(pref)。 - Repository 先调用 Serializer 将
pref对象转为 JSON 字符串或 Protobuf 字节数组。 - Repository 将字节数组交给 FileManager,传入预定义文件路径(如
app_data/prefs/user_pref.json)。 - FileManager 检查目录是否存在,若不存在则创建;生成临时文件
user_pref.tmp写入完整数据;写入成功后再 rename 为正式文件,保证原子性。 - 业务层收到成功回调,流程结束。
数据流方向为 内存对象 → 序列化字节 → 磁盘文件,读取时反向。全程为同步 I/O,不支持背压或异步批量。
原子写实现要点:写操作必须先写入临时文件,待 fsync / flush 确认磁盘落盘后,再执行 rename 覆盖目标文件。读取时只读取目标文件。这个模式提供了“写入期间崩溃不会损坏已有数据”的保障。
5. 选型权衡:JSON vs Protobuf
| 对比维度 | JSON | Protobuf |
|---|---|---|
| 可读性 | 极好,纯文本,可用任意编辑器查看调试 | 二进制不可读,必须借助 .proto + 工具解码 |
| 序列化性能 | 中等,大量反射或字符串解析 | 极快,二进制格式,无反射(语言绑定层优化) |
| 数据体积 | 大,含字段名括号与引号 | 小,仅存 field ID + varint 编码 |
| 模式演进 | 弱,新增字段需写兼容性逻辑 | 强,官方支持 optional / deprecated / oneof |
| 绑定成本 | 低,几乎无构建期依赖 | 高,需 proto 编译器生成桩代码,集成到构建系统 |
| 适用阶段 | 原型、调试、轻量配置 | 正式发布、关注体积与性能的模块 |
风险提示:不要在同一个 App 内混合使用 JSON 与 Protobuf 存储同一领域对象。这会增加双倍测试成本,且 Proto 结构很难被未部署 .proto 的设备直接读取。若团队缺乏 Proto 维护经验,建议从 JSON 起步,在性能基准测试数据明确后,再对瓶颈模块迁移至 Protobuf。
6. 风险点与应对
- 并发写覆盖:多线程或进程同时写同一文件,导致数据截断或丢失。→ 使用文件互斥锁(flock / LockFile),或在 Repository 层加互斥体。
- 写中断数据损坏:写入过程中崩溃,产生残缺文件。→ 强制执行“写临时 + rename”策略,读取时只读正式文件。
- 序列化版本不兼容:业务实体添加字段后,旧版本文件反序列化失败。→ 强制实施字段默认值、忽略未知字段策略;Protobuf 自带兼容,JSON 需手动处理。
- 文件碎片与垃圾积累:频繁写导致文件碎片或残留临时文件。→ FileManager 定期清理
*.tmp文件,并记录写次数触发碎片整理。 - 存储路径硬编码:硬件厂商路径策略变化(如 Android Scoped Storage)。→ 文件路径由系统 API 获取(
getFilesDir()/NSDocumentDirectory),不写死路径。
7. 落地步骤
- 划定边界:在架构文档中明确声明本模块不支持的场景(多进程、事务、跨实体查询)。
- 选择序列化协议:根据团队经验与性能目标,选定 JSON 或 Protobuf,编写序列化合约。
- 实现 FileManager 基座:封装路径管理、目录创建、原子写入、文件锁。编写单元测试模拟写入崩溃。
- 构建 Repository 适配层:针对每个业务实体编写 Repository 实现,接口统一为
save / load / delete。 - 集成到依赖注入容器:将 Repository 注册为单例或 Scope 实例,业务层通过接口引用。
- 编写验收测试:构建持续集成流水线,跑通数据写回读校验、边界值测试、并发写保护验证。
8. 验收标准
| 验收项 | 通过条件 |
|---|---|
| 写回读一致性 | 保存 100 次随机对象,100% 读取后字段值完全一致 |
| 写中断保护 | 写入中模拟崩溃,重启后文件数据为上一次合法版本或空文件 |
| 并发写保护 | 两个线程同时保存,不会出现截断文件,读取时总得到完整数据之一 |
| 序列化向前兼容 | 新版本 App 写入文件,老版本 App(已知字段)能正确读取默认值 |
| 文件路径正确性 | 所有文件位于系统沙箱内,无泄漏到公共存储区 |
| 性能基准 | 单次序列化+写盘 ≤ 50 ms(5KB 对象,中端设备) |
架构师备忘:文件存储方案是“最小依赖”的极致选择,但它的适用面也是所有方案中最窄的。在评审阶段,请务必问三个问题——① 是否有事务需求?② 是否有范围查询?③ 是否有跨进程需求?任何一个答案为“是”,立即关闭文件存储选项。