一、前置思考
1.1 弱网场景是移动应用体验的分水岭
地铁、地下停车场、隧道、乡村、跨境旅行------网络时有时无是常态。应用在弱网下"能不能用、数据对不对"决定了用户去留:
场景1: 地铁上看新闻 → 没网 → 直接白屏 → 卸载
场景2: 旅行App离线查攻略 → 数据是昨天的 → 用户投诉
场景3: 物流PDA离线签收 → 数据存本地 → 上线后要合并 → 冲突!
场景4: 地下车库导航 → 无网 → 关键数据要可用
1.2 离线优先(Offline-First)架构思想
传统架构: 网络是主, 缓存是辅 (无网即死)
离线优先: 本地是主, 网络是辅 (无网可用, 有网同步)
核心原则:
① 数据先写本地 (立即响应)
② 网络异步同步 (后台排队)
③ 冲突有策略 (不丢不覆盖)
④ 上线自动合并 (无感)
1.3 本文路线
深入弱网预加载、离线数据优先级、同步队列、冲突合并策略,落地离线优先架构。
二、核心原理
2.1 弱网预加载策略
预加载 = 提前把可能用到的数据拉到本地
策略1: 连接时预加载 (WIFI 下全量预热)
策略2: 页面预加载 (进入列表页预取详情页数据)
策略3: 智能预加载 (基于用户习惯预测)
优先级:
WIFI: 预加载大体积 (视频/地图包)
蜂窝: 只预加载核心数据 (列表/关键详情)
弱网: 停止大文件, 保留文本核心
2.2 离线数据优先级
数据分级:
P0 关键数据 (必须离线可用): 账号、基础配置、离线地图
P1 常用数据 (强烈建议离线): 首页列表、已读消息
P2 扩展数据 (有网加载): 大图、视频、完整详情
存储分层:
P0 → 加密数据库 (可靠)
P1 → KV/磁盘缓存 (快速)
P2 → 不缓存, 按需加载
2.3 同步队列机制
本地写入 → 进入同步队列 (带操作类型 + 版本)
网络恢复 → 队列顺序同步 → 成功出队 → 失败重试
同步队列设计:
持久化: 队列存本地 (重启不丢)
幂等: 同一条数据重复同步不重复生效
顺序: 同一实体按操作顺序同步
重试: 指数退避, 上限封顶
2.4 冲突合并策略
冲突场景: 设备A离线改了订单, 设备B在线改了同一订单
策略1: LWW (最后写入胜) --- 简单但可能丢用户意图
策略2: 字段级合并 --- 不同字段各取所长
策略3: 操作级合并 --- 累加操作 (计数器)
策略4: 冲突提示 --- 无法自动合并时让用户选择
推荐: 按数据类型混合使用
三、源码/API 深度解析
3.1 离线优先写入层
import { distributedKVStore } from '@kit.ArkData';
// 同步队列条目
interface SyncOp {
id: string; // 全局唯一操作 id (幂等)
entity: string; // 实体类型
key: string; // 实体 key
op: 'put' | 'delete';
data: string; // 数据快照
ts: number; // 本地时间戳
retries: number;
}
class OfflineSyncQueue {
private kv: distributedKVStore.SingleKVStore | null = null;
private queue: SyncOp[] = [];
private readonly MAX_RETRY = 5;
async init(kv: distributedKVStore.SingleKVStore): Promise<void> {
this.kv = kv;
this.queue = await this.loadQueue(); // 从本地恢复
}
// 离线写入: 先落本地 + 入队
async enqueue(op: SyncOp): Promise<void> {
if (this.kv) { await this.kv.put('entity:' + op.key, op.data); }
this.queue.push(op);
this.persistQueue();
}
// 上线同步: 顺序出队
async flush(syncFn: (op: SyncOp) => Promise<boolean>): Promise<void> {
const pending = this.queue.slice();
for (const op of pending) {
const ok = await syncFn(op);
if (ok) {
this.queue = this.queue.filter(o => o.id !== op.id);
} else {
op.retries++;
if (op.retries >= this.MAX_RETRY) {
// 超限: 保留待人工处理
console.error('同步失败超限: ' + op.id);
}
}
}
this.persistQueue();
}
private persistQueue(): void {
// 队列持久化到本地存储
}
private async loadQueue(): Promise<SyncOp[]> {
return [];
}
}
3.2 弱网检测与预加载
import { connection } from '@kit.NetworkKit';
// 网络状态监听: 恢复时触发同步, WIFI 时触发预加载
function watchNetwork(onWifi: () => void, onCellular: () => void, onOffline: () => void): void {
connection.on('netAvailable', (info: connection.NetHandle) => {
const caps = connection.getConnectionProperties(info);
if (caps && caps.linkName === 'wlan') {
onWifi(); // WIFI: 预加载大文件
} else {
onCellular(); // 蜂窝: 预加载核心
}
});
connection.on('netLost', () => { onOffline(); });
}
3.3 冲突合并实现
// 字段级合并: 不同字段各取修改时间新者
interface DocVersion {
fields: Map<string, { value: string; ts: number }>;
}
function mergeFieldLevel(local: DocVersion, remote: DocVersion): DocVersion {
const merged = new Map(local.fields);
remote.fields.forEach((v, key) => {
const lv = local.fields.get(key);
if (!lv || v.ts > lv.ts) {
merged.set(key, v); // 远程该字段更新
}
});
return { fields: merged };
}
// 操作级合并 (计数器): 差值累加
function mergeCounter(local: number, remote: number, localStart: number, remoteStart: number): number {
// 各自的增量叠加, 而不是覆盖
return local + (remote - remoteStart);
}
四、企业级实战落地
4.1 离线优先架构全景
┌──────────────────────────────────────────────┐
│ 业务层: 列表/详情/操作 (先读本地, 立即响应) │
├──────────────────────────────────────────────┤
│ 数据层: │
│ 本地读取 (P0/P1 缓存) → 命中? → 返回 │
│ → 未命中 → 有网? → 拉远端 → 回填本地 │
│ → 无网 → 返回"离线态"降级提示 │
├──────────────────────────────────────────────┤
│ 同步层: │
│ 写入 → 本地立即生效 + 入同步队列 │
│ 恢复网络 → 队列同步 → 冲突合并 → 收敛 │
├──────────────────────────────────────────────┤
│ 存储层: P0 数据库 / P1 KV / P2 不缓存 │
└──────────────────────────────────────────────┘
4.2 数据优先级落地
| 数据类型 |
优先级 |
离线策略 |
存储 |
| 账号/Token |
P0 |
必须离线 |
加密 KV |
| 首页列表 |
P1 |
预热 + 缓存 |
KV + 磁盘 |
| 已读消息 |
P1 |
本地优先 |
数据库 |
| 详情/大图 |
P2 |
在线加载 |
不缓存 |
4.3 同步冲突解决矩阵
| 数据类型 |
合并策略 |
说明 |
| 用户设置 |
LWW |
后改覆盖 |
| 计数器(点赞/步数) |
操作合并 |
增量叠加 |
| 文档字段 |
字段级合并 |
各取最新 |
| 订单状态 |
状态机校验 |
非法迁移拒绝 |
| 待办事项 |
冲突提示 |
用户选择 |
4.4 离线体验指标
| 指标 |
目标 |
| 离线可读率 |
P0/P1 数据 100% 可读 |
| 离线操作成功率 |
写入本地 100% 成功 |
| 同步成功率 |
恢复后 ≥ 99% |
| 冲突率 |
字段级 < 5% |
| 同步延迟 |
恢复网络后 < 10s |
五、问题排查与性能优化
| 坑 |
现象 |
原因 |
解决 |
| 离线白屏 |
无网无数据 |
未预加载 |
P0/P1 预热 |
| 数据丢失 |
重启后离线数据没了 |
队列未持久化 |
队列落本地 |
| 重复同步 |
同数据同步两次 |
无幂等 |
全局操作 id |
| 同步乱序 |
后写先同步 |
无顺序控制 |
按实体排序 |
| 覆盖丢失 |
离线修改被覆盖 |
LWW 后写覆盖 |
字段级合并 |
| 同步卡死 |
一条坏数据阻塞队列 |
无超限处理 |
重试上限 + 隔离 |
| 内存膨胀 |
离线缓存无限增长 |
无容量/过期 |
分级 TTL + LRU |
5.1 队列持久化与崩溃恢复
同步队列必须落本地 (KV/文件)
崩溃恢复: 启动时加载队列 → 未同步的重新入队
幂等保障: 每条操作带全局 id, 服务端去重
5.2 弱网降级策略
弱网判定: RTT > 阈值 / 丢包率 / 连续失败
降级动作:
① 停止大文件传输
② 图片降为缩略图
③ 请求合并 (批量接口)
④ 进入只读模式 (禁用写操作) 或 离线模式 (全部本地)
5.3 同步时机选择
触发时机:
网络恢复 (netAvailable)
应用回到前台 (onForeground)
定时兜底 (每 30 分钟)
用户手动刷新
避让策略:
弱网不重试 (减少失败请求)
WIFI 才同步大文件
低电量延迟同步
六、高阶总结与最佳实践
- 离线优先是架构不是补丁:本地为主、网络为辅,无网可用、有网收敛。
- 数据分级定离线策略:P0 必离线、P1 常缓存、P2 在线加载。
- 同步队列三要素:持久化、幂等、有序,重试有上限。
- 冲突合并按类型:设置 LWW、计数器叠加、文档字段合并、订单状态机。
- 弱网智能降级:检测网络质量,分级预加载、避让同步。
一句话记住:离线优先四步走------数据先落本地、网络异步同步、冲突按类型合并、弱网智能降级;让应用在没网时也能"可用、可信、不丢"。