移动端架构体系(二):本地持久化与动态部署

一、问题域与成功标准

1.1 为什么持久化会"拖垮"架构

本地持久化一旦和业务 UI、网络层、账号体系缠在一起,典型症状包括:

同一概念在 DTO / 域模型 / DB 行 / 列表 Cell 模型 之间反复拷贝,字段改名要改十几处。
改了下属性就落库列表滑动触发了写 ,线上出现难复现的数据损坏。

迁移脚本散落在各业务,跨版本升级只能靠用户卸载重装

同步与本地事务顺序混乱,出现幽灵数据、重复、删不掉、两台设备不一致。

因此,持久化方案的目标不应只写用SQLite,而应写清 边界、语义、失败模式与演进策略。

1.2 可度量的成功标准

维度 说明
可维护性 改表结构、加索引、加字段有固定入口;强弱业务可拆分迁移。
正确性 读写语义明确(快照 vs 可写草稿);崩溃与杀进程下事务可恢复或可追溯。
可测试性 Repository 可在内存/临时 DB 上跑单测;迁移可表驱动测试。
可观测性 慢查询、迁移耗时、同步队列积压可度量。
合规与安全 敏感字段加密、备份策略、隐私清单与最小化采集可对齐。

二、持久化介质全景与选型决策

2.1 能力对照(抽象层,不限具体 API)

介质 典型 API / 框架 强项 弱项
键值 UserDefaults、NSUbiquitousKeyValueStore、SharedPreferences、DataStore Preferences 极小数据、配置、开关 无结构查询、易滥用成大 JSON
安全存储 Keychain、EncryptedSharedPreferences、Keystore 凭证、密钥材料 容量与语义不适合业务表
文件 JSON/plist、二进制、沙盒目录 大对象、导出、资源缓存 复杂查询成本高
嵌入式 DB SQLite(直接/Room/GRDB/FMDB)、Core Data、Realm、SwiftData(底层仍多为 SQLite) 条件查询、关系、迁移工具链 设计不当会变小后端复杂度

2.2 决策树

2.3 反模式清单

把业务列表整包序列化进 UserDefaults --- 版本升级、部分更新、多线程写均痛苦。

在 UI 层直接持有 FMDatabase / SQLiteDatabase / NSManagedObjectContext --- 生命周期与线程模型失控。

无事务的大批量写入 --- 卡顿、WAL 暴涨、中断后半写入状态。

迁移逻辑写在业务启动页 copy-paste --- 无法回归、无法跨版本组合验证。

同步与本地 CRUD 共用同一套随手改模型--- 无法区分"用户意图"与"服务端投影"。

三、架构分层

期望:持久层与业务层隔离、读写隔离、线程隔离、数据表达与数据操作隔离, DataCenter(强业务) + Table(弱业务) + Virtual Record(协议化载体)。

3.1 现代命名映射(便于 Android/iOS 统一讨论)

文中概念 常见现代实现
DataCenter FooRepository、FooDataStore、用例(UseCase)组合多个 DAO
Table / QueryCommand UserDao、OrderQueries、GRDB QueryInterface
Virtual Record PersistableRow 协议、toRow()/apply(row:)、DTO + Mapper
Database Pool 单写多读队列、Room 的 invalidation、Core Data stack

核心不是名字,而是 权限与职责:谁能改库、谁知道 SQL、谁知道"列表页怎么筛"。

3.2 推荐依赖方向(自上而下)

复制代码
Presentation (SwiftUI/UIKit / Compose/Fragment)
    → 只依赖用例接口或 ViewModel 所需模型
Application / Domain
    → Repository 接口(领域语言:saveDraft、publish、observeInbox)
Infrastructure / Data
    → Repository 实现:组合 DAO、同步网关、缓存策略
    → Local DataSource(SQLite/Room/GRDB) + Remote DataSource(API)

依赖倒置:Domain 定义 MessageRepository,Data 里实现;单元测试替换为 fake。

3.3 "胖 Model / 瘦 Model"与"去 Model 化"在持久层的统一说法

持久层的实用表述:

持久化模型(Storage Model):贴近表结构,稳定、可版本化,字段命名偏 DB。

领域模型(Domain Model):业务不变量、枚举状态机,不直接暴露给 SQL。

展示模型(View State):列表 diff、格式化文案、占位图状态。

去 Model 化在边界上体现为:跨层传输可用 字典/DTO/Row,但在 仓库内部 仍应有 显式映射,而不是"全场 NSDictionary"逃避类型。

四、读写隔离与对象语义(极易被低估)

4.1 两类"读结果"

  • 快照(Snapshot)

    从 DB 读出后 不可变(Swift struct + let,Kotlin data class copy,或只读接口)。UI 若编辑,先 copy → 编辑 → 显式 commit。

  • 草稿(Draft)

    明确生命周期:可能 从未持久化 或对应未同步的本地编辑缓冲

4.2 为什么要隔离"写路径"

  • 可在唯一写入口挂:审计日志、同步 enqueue、性能统计、数据校验。
  • 避免 ORM 跟踪器在 UI 线程隐式 save。
  • 与同步状态机 对齐:哪些写应产生 Outbox 事件,哪些只是本地缓存。

4.3 AOP 切点应落在何处

适合放在 Repository 实现或单一 DatabaseGateway:

  • willMutate / didMutate(table, op, ids)
  • 事务 begin/commit 包装
  • 调试构建下断言:主线程禁止写

五、并发模型与 SQLite 实务

5.1 SQLite 线程模型(概念层)

  • 单连接 + 串行写队列 在移动端往往 性价比最高:逻辑简单,性能通常够用。
  • 多连接读写需理解 WAL、busy handler、锁竞争;除非 profiling 证明瓶颈,否则不要先优化成多写。

5.2 跨线程传递规则

  • 传递 主键或值类型快照,不传递可变行对象。
  • iOS:Core Data 传 NSManagedObjectID 的经验可推广到一切 ORM。
  • Kotlin:Room 的 @Transaction 与协程 Dispatcher 要在规范里写死。

5.3 长事务与 UI 卡顿

  • 大批量导入:后台队列 + 单事务 + 分批 commit(若框架允许子批)。
  • 迁移:独立阶段,UI 显示正在准备数据或后台完成,避免首屏抢锁。

六、数据表达与数据操作分离(超越 Active Record)

6.1 Active Record 的问题在移动端如何体现

一个 User 类既 save() 又承担 avatarURL、isVIP 展示,又夹杂 tableName,结果是:

复用困难:想只在设置页持久化部分字段,却拖进整张表语义。

测试困难:new 一个对象就隐含 DB 副作用。

6.2 推荐拆分

类型 职责
UserTable / UserDao 按主键/条件查询、插入、更新、删除;不知道 UI
UserRepository 组合多表、处理同步标记、领域规则
User(域) / UserListItem(UI) 不含 save(),或仅含生成待写入命令的纯函数

6.3 Virtual Record 的当代实现要点

协议化能力可包含:

  • func rowPayload(for table: TableId) -> [String: Any] --- 一对多表写入时按表裁剪列。
  • func merge(other: Self, policy: MergePolicy) --- 多对一聚合。
  • static func from(row: Row) --- 统一构造。

这样 Controller 不堆拼字段,迁移时带走协议实现即可。

七、Schema 设计与性能(移动场景仍值得正经做)

7.1 纵切与横切

纵切:列过多按业务域拆表(用户基础 / 用户扩展),详情页再 join 或多次查在内存拼。

横切:按类型、时间、分表规则拆(如 messages_2026_03),移动端较少见,多见于日志或超大本地库。

7.2 索引与查询规范

为 WHERE / ORDER BY / JOIN ON 的实际路径建索引;用 EXPLAIN QUERY PLAN 验收。

避免在循环内单条插入;用 批量 insert + 事务。

7.3 大对象与附件

DB 存 路径、hash、mime、加密标志;二进制走文件或系统级缓存。

全文检索若必须本地做,评估 FTS5(SQLite)与包体、维护成本;否则上云检索。

7.4 加密

文件级:SQLCipher、Room 加密支持、iOS Data Protection 类。

字段级:仅极敏感列(如证件号)加密,密钥走 Keychain/Keystore。

明确 备份(iCloud/Android Backup)行为,避免以为加密了其实被备份明文

八、迁移(Migration)工程化

8.1 版本模型

App 版本 与 DB 版本 不必 1:1,但每一个 schema 变更 必须对应 单调递增的 DB version。

迁移步骤应是 纯函数列表:(oldVersion, newVersion) -> [Step]。

8.2 步骤类型

DDL:建表、加列、建索引。

DML 数据修复:填默认值、清洗脏数据。

重建表(SQLite 常见):改约束、改主键时整表复制。

8.3 跨版本组合与测试矩阵

用户可能从 v3 直接升到 v8,必须验证:

3→4→5→...→8 链式执行

每一步 幂等(重复跑不炸)在可能情况下成立,或明确不可重复

自动化:准备 golden fixture(旧版 DB 文件),跑迁移后做校验查询。

8.4 失败策略

可重试:磁盘满、I/O 错误 → 提示、稍后重试。

不可恢复:备份失败且 schema 不一致 → 记录 telemetry,清库 或 只读模式(需产品决策)。

九、数据同步

9.1 单向同步(设备为操作源,服务端确认)

适用:IM 发送、离线表单提交、本地优先写。

字段级建议:

字段/机制 作用
local_id (UUID) 客户端生成,映射服务端 id
server_id 回填
sync_status pending / confirmed / failed
dirty 是否有未上传变更
deleted 逻辑删除直至确认
dependency_id 处理包乱序
operation_log 表 发送窗口内允许继续编辑时的多操作队列

极端情况:确认包返回前用户又改 ------ 要么 UI 锁编辑,要么 新 operation 记录 再发,绝不能默默把 dirty 清掉。

9.2 双向同步(多端协作)

操作对象协议:

  1. 操作唯一 id
  2. 实体唯一 id(稳定、可跨端)
  3. 操作类型(insert/update/delete/语义操作如 archive)
  4. Payload(字段级 patch 优于整对象覆盖)
  5. 依赖 id
  6. 客户端时间戳(仅作提示,不是绝对真理)
  7. 实体版本 / 向量时钟 / 服务端 revision(强烈建议至少一种)
  8. 来源设备 id(排错与去重)

本地双队列:

  • Inbox(待执行):服务端拉下的操作。
  • Outbox(待同步):本地上传。

顺序:通常 先 drain Inbox 再推 Outbox(或按业务定义交错规则),并在规范里写死,避免自己刚上传的又被下行覆盖类 bug。

9.3 冲突解决策略选型

策略 适用 风险
LWW(最后写入赢) 简单配置、非关键文案 静默丢编辑
字段级 merge 结构化文档、可拆分字段 需 schema 支持
操作变换 / CRDT 实时协作、富文本 实现与测试成本高
用户选择 高价值内容 UX 负担

不要同步 SQL 语句:冲突分析、安全审计、版本兼容都会变成灾难。

9.4 离线队列与重试

指数退避 + 抖动,区分 可重试错误 与 业务 4xx。

幂等键:服务端按 client_op_id 去重。

队列上限与合并:同一实体的多条 patch 在发送前可合并(注意依赖关系)。

十、测试策略(没有这部分,架构文不完整)

10.1 单测

Fake Clock、IdGenerator 注入 Repository。

内存数据库(:memory:)或临时文件 DB。

10.2 迁移测试

资产化 vN.sqlite,迁移到最新,断言行数、关键列、索引存在。

10.3 同步测试

模拟乱序包、重复包、失败重试、中途杀进程。

状态机属性测试(property-based)若有条件可上。

十一、动态部署:手段、边界与推荐组合

11.1 目标拆分

动态常混指三件事,应分开决策:

  • 内容动态:文案、运营页、帮助、活动规则。
  • 配置动态:开关、实验、参数、路由表。
  • 逻辑动态:改变原生代码路径、下发可执行脚本修补方法。

只有 1+2 是默认安全区;3 在 iOS 上基本不可作为正规方案。

11.2 方案谱系

方案 能力 主要限制
纯 H5 / PWA 迭代最快 能力、体验、离线复杂
Hybrid + JSBridge 内容+部分逻辑,复用 Native 能力 性能、复杂交互、调试成本
服务端驱动 UI(JSON DSL) 换肤、运营布局、表单 事件与原生能力需预注册
远程配置 / Feature Flag 灰度、降级、参数 不是万能逻辑补丁
内置热更 bundle(RN/Flutter) 在合规前提下更新 JS bundle 不能随意加新的原生插件而不发版
方法替换 / 下载动态库 历史上用于热修 审核与签名模型下不可依赖

11.3 微服务化 Native

把 Native 能力拆成 稳定、版本化、弱业务 的 service:

  • auth.getSession
  • pay.startCheckout
  • nav.open
  • storage.putFile
  • device.getSafeArea

H5 或 DSL 编排业务流程,而不是每个页面穿透调用任意 Objective-C/Swift。

11.4 安全与治理

Bridge 白名单 + 参数 schema 校验(防 XSS/注入式调用)。

远程资源 签名与完整性(hash + 公钥验签)。

最小权限:WebView 不开放整文件系统。

回滚:配置带版本号,客户端保留上一版。

11.5 Android 与 iOS 差异(简述)

Android 在历史上对 动态加载 dex 等更敏感,Google Play 同样有政策约束。

iOS 对新解释执行代码 限制更严;Hybrid 与配置驱动 仍是主力。

十二、端到端示例(伪流程)

场景:设置页多表单片段 + 用户详情纵切两表 + 单向同步。

  1. SettingsViewModel 调 UserRepository.loadProfile() → 返回 快照。
  2. 用户编辑 → Draft 状态仅在内存,点保存 → repository.saveProfile(draft):
    • 单事务写 user_core、user_prefs
    • Outbox 追加 UpsertUser 操作(含 local_op_id)
  3. 同步器上传,成功后 映射 server_id,清 dirty,删 Outbox 行。
  4. 若失败 → UI 展示重试;允许继续编辑则 新 op 而非覆盖旧 op 状态。

十三、落地路线图

  • 第一阶段(规范)
    选型矩阵、禁止项、目录结构(data/local、data/sync、domain)。
  • 第二阶段(骨架)
    Repository + DAO + DTO + 单写队列;迁移框架就位。
  • 第三阶段(同步)
    Outbox/Inbox、幂等、乱序依赖、冲突策略文档化。
  • 第四阶段(动态)
    Remote Config + Hybrid 容器 + Bridge 版本化;明确不做 的灰区(热修)。
    持续

慢查询与迁移 dashboard;线上数据异常 SOP。

十四、结语

本地持久化的质量,本质是 边界是否清晰、语义是否可证明;动态部署的质量,本质是 把"可变"限制在商店与风险模型允许的层。

相关推荐
一直在想名2 小时前
Flutter 框架跨平台鸿蒙开发 - 人生RPG - 把日常任务变成RPG任务,完成获得经验值
flutter·华为·harmonyos
李李李勃谦2 小时前
Flutter 框架跨平台鸿蒙开发 - 星座运势应用
flutter·华为·harmonyos
独特的螺狮粉2 小时前
Flutter 框架跨平台鸿蒙开发 - 心理健康测试应用开发文档
flutter·华为·harmonyos
AI_零食2 小时前
Flutter 框架跨平台鸿蒙开发 - 鸿蒙版本跳棋游戏应用
学习·flutter·游戏·华为·交互·harmonyos
2301_822703202 小时前
开源鸿蒙跨平台Flutter开发:基因序列比对基础:Needleman-Wunsch 算法的 Dart 实现
算法·flutter·开源·鸿蒙
恋猫de小郭2 小时前
抖音“极客”适配 Android 5 ~ 9 等老机型技术解读,都是骚操作
android·前端·flutter
autumn20052 小时前
Flutter 框架跨平台鸿蒙开发 - 露营助手应用
flutter·华为·harmonyos
2301_822703202 小时前
开源鸿蒙跨平台Flutter开发:脑电波 (EEG) 实时绘制:Flutter Canvas 多波形同步渲染与 Isolate 线程隔离
flutter·华为·开源·harmonyos·鸿蒙
autumn20052 小时前
Flutter 框架跨平台鸿蒙开发 - 油耗记录应用
flutter·华为·harmonyos