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
相关推荐
aqi006 小时前
鸿蒙版本的JSBridge兼容与安卓配套的H5啦
android·华为·harmonyos·鸿蒙·移动应用
熊猫钓鱼>_>7 小时前
Flutter app_settings 鸿蒙适配实战:Intent 体系到 Want 的跨越
flutter·华为·harmonyos·openharmony·intent·want
贾伟康9 小时前
【句匠|20】HarmonyOS ArkTS AppGallery 发布复查实战:核对包名、版本、设备、素材和离线声明
harmonyos·arkts·应用上架·appgallery·发布审核
贾伟康10 小时前
【句匠|15】HarmonyOS ArkTS 本地状态持久化实战:让保存、删除和页面返回后的数据即时一致
harmonyos·arkts·数据持久化·appstorage·preferences
贾伟康13 小时前
【句匠|16】HarmonyOS ArkTS 多设备布局实战:适配手机、平板和 PC/2in1 的窗口变化
harmonyos·arkts·arkui·响应式布局·多设备适配
小雨青年13 小时前
【HarmonyOS 7 平行视界深度实战】03 购物模式怎么实现连续浏览和左右推挤
华为·harmonyos
SuperHeroWu715 小时前
HarmonyOS Dev Assistant (HarmonyOS开发助手)如何打通元服务开发全流程
ai·agent·harmonyos·vs code·元服务·hbuilderx·assistant
熊猫钓鱼>_>15 小时前
用 OHAudioSuite 空间渲染节点搭一座「3D 有声博物馆」——从三种模式到 FreeBuds 头追的实战记录
c++·3d·ai·harmonyos·arkts·audio·ohaudiosuite
OH_TPC16 小时前
HarmonyOS APP开发---"心动卡"社交匹配App,需要用到这个库
harmonyos
光锥智能16 小时前
HUAWEI WATCH 6系列正式发布:鸿蒙AI手表,智慧健康旗舰
人工智能·华为·harmonyos