HarmonyOS 本地备份与系统备份:BackupExtensionAbility、快照导出和恢复

HarmonyOS 本地备份与系统备份:BackupExtensionAbility、快照导出和恢复

卡片工具类应用的数据量不大,但用户对"不要丢卡片"非常敏感。这个项目里做了两层备份:应用内的最近一次本地快照,以及 HarmonyOS 系统备份能力。两者都复用 AppDataService 的状态导出和恢复逻辑。

备份配置先打开

系统备份能力需要在 module.json5 里注册 backup extension:

复制代码
{
  "name": "EntryBackupAbility",
  "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
  "type": "backup",
  "exported": false,
  "metadata": [
    {
      "name": "ohos.extension.backup",
      "resource": "$profile:backup_config"
    }
  ]
}

同时 backup_config.json 里允许备份恢复:

复制代码
{
  "allowToBackupRestore": true
}

这两个文件缺一不可。

系统备份能力:EntryBackupAbility

EntryBackupAbility.ets 很薄,只负责生命周期和日志:

复制代码
export default class EntryBackupAbility extends BackupExtensionAbility {
  async onBackup() {
    try {
      appDataService.initialize(this.context);
      const backupPath: string = appDataService.exportSystemBackup(this.context.backupDir);
      hilog.info(DOMAIN, 'testTag', 'onBackup ok %{public}s', backupPath);
    } catch (error) {
      hilog.error(DOMAIN, 'testTag', 'onBackup failed %{public}s', JSON.stringify(error));
    }
  }

  async onRestore(bundleVersion: BundleVersion) {
    try {
      appDataService.initialize(this.context);
      const restored: boolean = appDataService.restoreSystemBackup(this.context.backupDir);
      hilog.info(DOMAIN, 'testTag', 'onRestore ok %{public}s %{public}s', JSON.stringify(bundleVersion), `${restored}`);
    } catch (error) {
      hilog.error(DOMAIN, 'testTag', 'onRestore failed %{public}s', JSON.stringify(error));
    }
  }
}

重点是:系统回调也要先初始化数据服务。Backup extension 不能假设主应用页面已经运行。

应用内备份:保存最近一次快照

备份页的按钮很直接:

复制代码
private manualBackup(): void {
  appDataService.backupNow();
  this.refreshData();
}

private restoreBackup(): void {
  appDataService.restoreLastBackup();
  this.refreshData();
}

服务层执行备份时导出 JSON 快照,并记录备份大小:

复制代码
backupNow(): BackupMetaModel {
  const now: Date = new Date();
  this.state.backupMeta.lastBackupAt = formatDateTime(now);
  this.state.backupMeta.lastBackupSize = '';
  const snapshot: string = this.exportSnapshot();
  this.state.backupMeta.lastBackupSize = this.formatSize(snapshot.length);
  this.recordActivity('backup', '执行本地备份', 3, now);
  this.persistLastBackupSnapshot(snapshot);
  this.persistState();
  return this.cloneBackupMeta(this.state.backupMeta);
}

注意先设置时间,再导出快照,最后写入状态和最近快照。

系统备份:写入 backupDir

系统备份使用系统传入的 backupDir

复制代码
exportSystemBackup(backupDirectory: string): string {
  const snapshot: string = this.exportSnapshot();
  const targetPath: string = joinPath(backupDirectory, SYSTEM_BACKUP_FILE);
  const handle: fileIo.File = fileIo.openSync(
    targetPath,
    fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC
  );
  try {
    fileIo.writeSync(handle.fd, snapshot);
    fileIo.fsyncSync(handle.fd);
  } finally {
    fileIo.closeSync(handle);
  }
  return targetPath;
}

这里用了 fsyncSync(),目的是尽量保证文件落盘后再结束回调。对备份这种操作来说,写入完整性比省一点时间更重要。

恢复:读快照后统一 import

系统恢复读取同一个文件:

复制代码
restoreSystemBackup(backupDirectory: string): boolean {
  const targetPath: string = joinPath(backupDirectory, SYSTEM_BACKUP_FILE);
  try {
    const snapshot: string = fileIo.readTextSync(targetPath);
    if (!snapshot.length) {
      return false;
    }
    this.importSnapshot(snapshot);
    this.state.backupMeta.lastBackupSize = this.formatSize(snapshot.length);
    this.state.backupMeta.lastRestoreAt = formatDateTime(new Date());
    this.recordActivity('restoreBackup', '从系统备份恢复数据', 3, new Date());
    this.persistState();
    return true;
  } catch (_error) {
    return false;
  }
}

本地恢复和系统恢复都应该走同一套 importSnapshot(),这样版本兼容、normalize 和异常处理不会分叉。

备份页展示的是当前本地状态

BackupPage.ets 页面读取摘要卡和元信息:

复制代码
@State summaryCard: ShowcaseCardModel = appDataService.getBackupSummaryCard();
@State backupMeta: BackupMetaModel = appDataService.getBackupMeta();

private refreshData(): void {
  this.summaryCard = appDataService.getBackupSummaryCard();
  this.backupMeta = appDataService.getBackupMeta();
}

按钮操作后立即 refreshData(),页面就能看到最新备份时间、备份大小和自动备份状态。

自动备份开关只改状态,不做后台任务

页面里有"自动备份"按钮:

复制代码
appDataService.setAutoBackupEnabled(!this.backupMeta.autoBackupEnabled);
this.refreshData();

当前实现只是保存开关状态,并未接后台定时任务。交付记录里需要明确这一点,避免把 UI 状态误认为已经有系统级自动调度。

常见坑

  1. Backup extension 里忘记 appDataService.initialize(this.context)
  2. module.json5 配了 backup,但 backup_config.json 缺失。
  3. 系统备份和应用内备份使用两套 JSON,导致恢复逻辑不一致。
  4. 恢复后不调用 persistState(),页面刷新后状态又回退。
  5. 备份大小在导出前计算,导致显示不准确。

验证建议

按顺序验证:

复制代码
D:\dev\command-line-tools\bin\hvigorw.bat assembleHap --no-daemon --stacktrace

手工检查:

  1. 打开备份页,点击"立即备份",最近备份时间更新。
  2. 修改卡片后点击"恢复最近备份",确认数据回到备份状态。
  3. 切换自动备份按钮,状态文案同步变化。
  4. 系统备份能力触发时,project028-backup.json 能写入 backupDir。
  5. 恢复后统计页活动日志增加恢复记录。

基础链路小结

这个项目的备份设计很简单:整份应用状态就是备份快照。应用内备份把快照存在 Preferences,系统备份把快照写入 backupDir 文件。两者都复用服务层导出和恢复逻辑。

对轻量 HarmonyOS 工具应用来说,这比引入复杂备份模型更稳,也更容易验证。

备份页要同时满足用户体验和系统备份能力

备份恢复不能只写"点击按钮保存快照"。Project028 的备份模块分两条线:应用内手动备份/恢复,以及 HarmonyOS 系统备份 Ability。前者解决用户主动操作,后者服务系统迁移和平台能力;两者都应该回到 AppDataService,不要在页面和 Ability 中各写一套序列化逻辑。

应用内备份由 BackupPage.ets 调用 appDataService.backupNow()restoreLastBackup()。页面只负责触发、刷新和 Toast,不应该直接拼装快照。这样能保证备份元数据、活动记录、持久化动作都由服务层统一维护。

复制代码
private manualBackup(): void {
  this.applyBackupMeta(appDataService.backupNow());
  this.showBackupToast('已完成本地备份');
  this.reloadBackupPage();
}

private restoreBackup(): void {
  const restored: boolean = appDataService.restoreLastBackup();
  this.refreshData();
  this.showBackupToast(restored ? '已恢复最近备份' : '暂无可恢复的本地备份');
}

系统备份则在 EntryBackupAbility.ets 中完成。Ability 初始化服务层后调用 exportSystemBackup(this.context.backupDir)restoreSystemBackup(this.context.backupDir)。这说明备份目录由系统上下文提供,业务侧只需要把当前状态导出为稳定快照。

复制代码
onBackup() {
  appDataService.initialize(this.context);
  const backupPath: string = appDataService.exportSystemBackup(this.context.backupDir);
}

onRestore(bundleVersion: BundleVersion) {
  appDataService.initialize(this.context);
  const restored: boolean = appDataService.restoreSystemBackup(this.context.backupDir);
}

还有一个审核视角需要单独说明:备份页有说明性条目时,点击必须有反馈。AGC 审核常会点页面上的每个看起来可交互的区域;如果 InfoRow 有视觉点击感却没有响应,容易被归类为"点击无反应"。Project028 后续修复中为 InfoRow 增加 BackupInfoAction 和 Toast,就是这一点的体现。

落地检查清单

  • 是否区分应用内备份和系统备份 Ability。
  • 是否说明备份快照只能由服务层生成,页面不直接拼 JSON。
  • 是否覆盖空备份时的恢复结果。
  • 是否写到审核体验:所有可点击行都要有反馈。
  • 是否覆盖真实路径:BackupPage.etsAppDataService.etsEntryBackupAbility.ets
相关推荐
澹然疏离2 小时前
HarmonyOS应用《民族图鉴》开发第33篇:StorageService——本地存储服务封装
harmonyos
Nturmoils2 小时前
Agent 不只是聊天框:用 ArkUI 做一个鸿蒙任务工作台
harmonyos
GitCode官方2 小时前
开源鸿蒙跨平台直播| RN 三方库开源鸿蒙标准化适配实战分享
华为·开源·harmonyos·atomgit
不才难以繁此生5 小时前
ArkUI 列表性能实战|中式美食菜谱列表如何拆组件、控重绘和稳住滑动体验
harmonyos·arkts·状态管理·foreach·arkui·列表性能·组件拆分
Ww.xh5 小时前
DevEco Studio虚拟机部署指南
华为·harmonyos
yuanlaile5 小时前
前端转 Flutter 踩坑实录:一套可落地 Flutter 鸿蒙 App 完整学习路线,建议收藏!
flutter·harmonyos·flutter开发鸿蒙·跨平台开发app·flutter深入学习
FrameNotWork5 小时前
HarmonyOS 6.1 数据持久化与首选项实战:Preferences 和 RDB 的那些坑
华为·harmonyos
爸爸6195 小时前
08 ArkTS 类与静态方法设计:单例、DAO 与设计令牌
华为·harmonyos·鸿蒙系统
ZZZMMM.zip5 小时前
基于鸿蒙HarmonyOS NEXT开发AI天气助手应用:智能出行新体验与鸿蒙Flutter框架跨端实践
人工智能·flutter·华为·harmonyos·鸿蒙系统