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 状态误认为已经有系统级自动调度。
常见坑
- Backup extension 里忘记
appDataService.initialize(this.context)。 module.json5配了 backup,但backup_config.json缺失。- 系统备份和应用内备份使用两套 JSON,导致恢复逻辑不一致。
- 恢复后不调用
persistState(),页面刷新后状态又回退。 - 备份大小在导出前计算,导致显示不准确。
验证建议
按顺序验证:
D:\dev\command-line-tools\bin\hvigorw.bat assembleHap --no-daemon --stacktrace
手工检查:
- 打开备份页,点击"立即备份",最近备份时间更新。
- 修改卡片后点击"恢复最近备份",确认数据回到备份状态。
- 切换自动备份按钮,状态文案同步变化。
- 系统备份能力触发时,
project028-backup.json能写入 backupDir。 - 恢复后统计页活动日志增加恢复记录。
基础链路小结
这个项目的备份设计很简单:整份应用状态就是备份快照。应用内备份把快照存在 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.ets、AppDataService.ets、EntryBackupAbility.ets。
