方案一:文件存储(JSON / Protobuf)——适用场景与架构边界

1. 方案边界定义

文件存储方案是最原始也最灵活的本地持久化手段。其核心思路是:将结构化或半结构化的数据对象,序列化为字节流写入磁盘文件,读取时再反序列化为内存对象。本方案聚焦于直接文件 I/O + 序列化协议这一组合,不依赖任何数据库引擎或 ORM 框架。

维度边界说明
数据规模单文件 ≤ 100 MB,总数据量 ≤ 1 GB。超过该阈值应评估 SQLite 或 LevelDB。
并发模型仅支持单进程读写。多进程场景需额外实现文件锁(flock / LockFile)。
查询需求无跨实体关联查询、无复杂过滤条件。仅支持全量加载后线性扫描。
事务语义无 ACID 保证。如需原子写,必须手工实现 write-ahead + rename 模式。
使用场景举例App 配置缓存、用户偏好、离线草稿箱、轻量级埋点日志、模型本地快照。
架构决策点:文件存储不是“数据库”,它是一个序列化管道。架构师必须在方案选型阶段就明确它的边界——凡涉及事务、关联、范围查询、多进程并发四项能力中的任意一项,都需要立即淘汰本方案。

2. 分层结构与组件职责

整个文件存储方案在 App 架构中占据基础设施层(Infrastructure)的位置,向上为业务层提供仓储(Repository)接口。内部又分为三层:

  1. 序列化层(Serializer):负责对象 ↔ 字节流的编码/解码。JSON 使用反射或手动映射,Protobuf 使用预定义 .proto 生成的代码。
  2. 文件管理层(FileManager):封装文件路径、创建、写入、读取、删除操作;处理同名覆盖、目录自动创建、文件锁。
  3. 仓储适配层(Repository Adapter):将序列化 + 文件操作合并为业务可理解的接口,如 UserPreferencesRepository.save(key, obj)。
文件存储方案 — 分层架构图 业务层(ViewModel / UseCase) 仓储适配层 (Repository) 文件管理层 (FileManager) 序列化层 (Serializer) JSON(反射/Mapping) Protobuf(.proto 生成代码)

3. 组件职责矩阵

组件职责不允许做的事
Serializer对单个实体进行 encode / decode,支持版本兼容(字段忽略/默认值)不处理文件路径、不管理缓存、不触发 I/O
FileManager文件路径拼接、目录创建、原子写(写临时 → rename)、文件锁不涉及业务实体结构、不进行序列化/反序列化
Repository组合序列化+文件操作,对外暴露 load()、save()、delete()不暴露底层文件句柄、不混合多种序列化协议

4. 数据流与控制流

以“保存用户偏好”为例,典型控制流如下:

  1. 业务层调用 UserPrefRepo.save(pref)。
  2. Repository 先调用 Serializer 将 pref 对象转为 JSON 字符串或 Protobuf 字节数组。
  3. Repository 将字节数组交给 FileManager,传入预定义文件路径(如 app_data/prefs/user_pref.json)。
  4. FileManager 检查目录是否存在,若不存在则创建;生成临时文件 user_pref.tmp 写入完整数据;写入成功后再 rename 为正式文件,保证原子性。
  5. 业务层收到成功回调,流程结束。

数据流方向为 内存对象 → 序列化字节 → 磁盘文件,读取时反向。全程为同步 I/O,不支持背压或异步批量。

原子写实现要点:写操作必须先写入临时文件,待 fsync / flush 确认磁盘落盘后,再执行 rename 覆盖目标文件。读取时只读取目标文件。这个模式提供了“写入期间崩溃不会损坏已有数据”的保障。

5. 选型权衡:JSON vs Protobuf

对比维度JSONProtobuf
可读性极好,纯文本,可用任意编辑器查看调试二进制不可读,必须借助 .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. 落地步骤

  1. 划定边界:在架构文档中明确声明本模块不支持的场景(多进程、事务、跨实体查询)。
  2. 选择序列化协议:根据团队经验与性能目标,选定 JSON 或 Protobuf,编写序列化合约。
  3. 实现 FileManager 基座:封装路径管理、目录创建、原子写入、文件锁。编写单元测试模拟写入崩溃。
  4. 构建 Repository 适配层:针对每个业务实体编写 Repository 实现,接口统一为 save / load / delete。
  5. 集成到依赖注入容器:将 Repository 注册为单例或 Scope 实例,业务层通过接口引用。
  6. 编写验收测试:构建持续集成流水线,跑通数据写回读校验、边界值测试、并发写保护验证。

8. 验收标准

验收项通过条件
写回读一致性保存 100 次随机对象,100% 读取后字段值完全一致
写中断保护写入中模拟崩溃,重启后文件数据为上一次合法版本或空文件
并发写保护两个线程同时保存,不会出现截断文件,读取时总得到完整数据之一
序列化向前兼容新版本 App 写入文件,老版本 App(已知字段)能正确读取默认值
文件路径正确性所有文件位于系统沙箱内,无泄漏到公共存储区
性能基准单次序列化+写盘 ≤ 50 ms(5KB 对象,中端设备)
架构师备忘:文件存储方案是“最小依赖”的极致选择,但它的适用面也是所有方案中最窄的。在评审阶段,请务必问三个问题——① 是否有事务需求?② 是否有范围查询?③ 是否有跨进程需求?任何一个答案为“是”,立即关闭文件存储选项。