Skip to content

网络层与本地存储

客户端最容易被低估的两件事:网络层要按「弱网是第一现场」来设计存储要按「设备可能随时消失」来设计。本页把各端通用(iOS / Android / Flutter / RN)的网络与存储工程要点收在一起,平台示例标注对应 API,细节可回到各平台页。

网络栈分层

客户端网络不是「发个请求」,而是可分层治理的系统:

职责要点
业务请求层参数组装、返回模型、错误语义一处封装,别每个页面自己拼 URL
传输会话层超时、重试、头注入、日志拦截器/中间件统一做
缓存层HTTP 缓存、内存/磁盘缓存命中本地先行(见下)
连接感知层联网状态、网络切换NWPathMonitor / Connectivity
传输层TLS、socket交给系统与框架,别手写

统一「错误语义」是分层最重要的小事:给 UI 一个结构化的失败原因(无网 / 超时 / 服务端错误码 / 业务拒绝),而不是让 UI 解析字符串。

swift
enum APIError: Error { case noNetwork, timeout, server(Int), business(String) }

弱网策略(比强网更该设计)

移动网络不是 Web 的网:电梯、地铁、跨网切换是常态。

问题工程对策
无网或极弱连接状态变化驱动:先给缓存 + 离线标记,恢复后自动补拉
超时误判分层超时:连接/读/总超时分开;弱网放宽读超时
瞬时抖动失败自动重试:指数退避 + 抖动,最多 2-3 次;写请求要幂等才能重试
用户不耐烦重试请求去重/合并:同一资源在途时并发请求只发一次
列表滚动连发节流与预取:滚近底部才拉下一页,图片按可视区加载

一个「重试要幂等」的反例:支付/下单这类写请求,如果客户端在服务端已成功后重试,会导致重复扣款——写请求必须有幂等键(由业务生成,服务端去重),这套思想与后端消息队列的幂等设计同源。

缓存体系:四层

载体用途失效
内存缓存NSCache / 自建 LRU高频小数据、图片解码结果内存告警即清,容量上限
磁盘缓存沙盒 cacheDir / 自建图片、大 JSONLRU 淘汰 + 容量上限
HTTP 缓存URLCache / OkHttp cache服务端带头信息的响应ETag / Last-Modified
业务缓存本地库/文件首页、feed 离线快照版本或时间窗

条件请求是「有缓存先展示、后台静默刷新」的关键:请求带 If-None-Match/If-Modified-Since,服务端返回 304 表示没变(复用缓存),200 表示更新。这比「总是全量重拉」省流量省时间。

原则:界面优先快(缓存),一致性交给后台刷新——用户可感知的延迟多来自不必要的等待,而非多 100ms 的渲染。

离线写入与冲突处理

离线做写入(评论、笔记、收藏)需要「先本地后同步」:

  • 本地先行:写入本地队列 + 乐观 UI(先显示成功),后台同步;
  • 幂等:队列里每条带本地幂等键;
  • 冲突必然发生,别假装不会:
    • 简单策略:last-write-wins(最后写入者胜)——要有明确的时间基准,否则两端各自「我赢」;
    • 服务端权威:服务器版本号/时间戳裁决,客户端检测到冲突走合并 UI(提示用户取舍);
    • 慎用「本地永远覆盖服务器」——除非产品明确「本机为准」(如本地笔记)。

序列化与模型兼容

后端接口演进会删字段/加字段,客户端必须能「向前兼容」:

  • 新加字段:客户端模型给默认值 / decodeIfPresent,老客户端解析新响应不崩;
  • 删字段/改语义:按服务端 API 版本规划,客户端显式处理旧值;
  • 本地缓存里的模型也要带 schema 版本,升级后做迁移(同桌面端配置迁移思路)。

本地存储盘点

平台轻量键值偏好推荐结构化 DB敏感数据
iOSUserDefaults小配置SwiftData / Core Data / GRDBKeychain
AndroidSharedPreferences(旧)DataStoreRoom / SQLiteKeystore + 加密存储
Fluttershared_preferences 插件同左sqflite / driftflutter_secure_storage(封装 Keychain/Keystore)
RNAsyncStorage / MMKVMMKVexpo-sqlite / WatermelonDBexpo-secure-store / react-native-keychain
桌面electron-store / tauri-plugin-store同左SQLiteOS 钥匙串/凭据管理器

选择铁律:

  • UserDefaults / DataStore / SharedPreferences 只放非敏感偏好(主题、语言、引导状态),不放 token/口令;
  • 敏感数据(token、私钥、口令)一律进 Keychain / Android Keystore / 对应 OS 安全存储——它们硬件/系统级加密且随账号走;就算数据库加密,密钥来源也应是 Keychain 而非硬编码;
  • 结构化、可查询、要事务 → SQLite 家族;纯键值 → 键值;大文件(图片/媒体)→ 文件系统 + 索引,别塞数据库。

敏感数据存放红线

  • 不在代码里硬编码密钥/证书;调试用不同 bundle/应用标识,避免真密钥混入(密钥保管见打包、签名与发布,「客户端安全与合规」主题在模块规划清单中待补齐);
  • 日志/崩溃上报里别打 token、完整手机号、密钥——统一脱敏器后再出日志;
  • 系统剪贴板、截屏、后台任务里对敏感界面做保护(Android FLAG_SECURE 等按产品需要);
  • 越狱/Root 设备上 Keychain/Keystore 防护减弱,风险由产品定(不在本页展开)。

常见坑速查

现象对策
弱网只调超时电梯里永远转圈/失败连接感知 + 缓存先行 + 恢复补拉
重试不幂等写请求重复执行扣两次写请求带幂等键,服务端去重
缓存全盘禁每次都全量拉数据分级缓存 + 条件请求
内存缓存无上限图片缓存把内存吃满容量上限 + 低内存自动清
离线直接禁用没网就白屏离线快照 + 乐观队列 + 恢复同步
token 存偏好被备份/明文可读进 Keychain/Keystore
忘记脱敏崩溃日志带出 token统一日志脱敏层
本地库塞大文件DB 膨胀、IO 变慢文件与索引分离
模型没默认值接口加字段后老包崩decode 给默认值/可选
冲突覆盖用户数据双端同时改互相丢服务端裁决 + 合并提示
退到前台不刷新数据永远是旧的scenePhase/App 生命周期挂刷新策略

检查清单

  • [ ] 网络统一封装:分层超时、错误语义结构化、日志脱敏;
  • [ ] 弱网:连接感知 + 缓存先行 + 恢复补拉已实现;
  • [ ] 写请求全部幂等,同步队列有重试与冲突处理;
  • [ ] 缓存四层有容量上限与低内存清理;
  • [ ] 模型解码带默认值与 schema 版本,本地缓存可迁移;
  • [ ] 敏感数据在 Keychain/Keystore,无硬编码密钥。

相关链接:iOS 开发 · Android 开发 · Flutter · React Native · 写作规范见客户端开发索引

基于 VitePress 构建 · 内容以知识共享方式沉淀