
HarmonyOS趣味相机实战第17篇BackupExtensionAbility备份恢复与Preferences版本迁移摘要本地相机应用通常同时保存两类数据真实照片文件以及用于页面展示的标题、时间、水印和文档索引。仅恢复其中一类会产生“列表存在但图片打不开”或“文件还在但应用相册为空”的半恢复状态。本文基于D:/APP/1quweixiangji趣味相机工程复盘EntryBackupAbility.ets、backup_config.json、PhotoAlbumService.ets与PhotoDocumentService.ets。当前工程已允许系统备份恢复但回调只记录日志因此本文重点补齐数据清单、版本迁移、恢复后校验、幂等初始化和故障演练。文章不假设系统一定会自动包含全部媒体文件而是以可验证的恢复结果作为完成标准。工程背景与源码定位文件当前责任备份恢复关注点entry/src/main/ets/entrybackupability/EntryBackupAbility.ets接收备份与恢复回调扩展能力入口和版本信息entry/src/main/resources/base/profile/backup_config.json允许备份恢复总开关不等于完整策略entry/src/main/module.json5注册 backup extensionAbilitymetadata 资源绑定entry/src/main/ets/service/PhotoAlbumService.ets照片元数据 Preferenceswatermark_camera_albumentry/src/main/ets/service/PhotoDocumentService.ets文档索引 Preferenceswatermark_camera_documentsentry/src/main/ets/model/DecorationModels.etsCapturedPhoto、CapturedDocument恢复后的模型兼容entry/src/main/ets/entryability/EntryAbility.ets启动期初始化两个服务恢复完成后的重载环境与数据边界项目当前值说明应用版本1.0.4versionCode为1000004target SDK6.0.2(22)当前工程配置应用模型Stage 模型UIAbility BackupExtensionAbility相册 Preferenceswatermark_camera_albumkey 为captured_photos文档 Preferenceswatermark_camera_documentskey 为converted_documents相册上限60 条slice(0, 60)文档上限80 条slice(0, 80)当前媒体模型元数据为主必须与真实文件策略分开验证一、先看当前扩展能力配置模块中注册{extensionAbilities:[{name:EntryBackupAbility,srcEntry:./ets/entrybackupability/EntryBackupAbility.ets,type:backup,exported:false,metadata:[{name:ohos.extension.backup,resource:$profile:backup_config}]}]}配置资源{allowToBackupRestore:true}扩展能力import{hilog}fromkit.PerformanceAnalysisKit;import{BackupExtensionAbility,BundleVersion}fromkit.CoreFileKit;exportdefaultclassEntryBackupAbilityextendsBackupExtensionAbility{asynconBackup(){hilog.info(DOMAIN,testTag,onBackup ok);awaitPromise.resolve();}asynconRestore(bundleVersion:BundleVersion){hilog.info(DOMAIN,testTag,onRestore ok %{public}s,JSON.stringify(bundleVersion));awaitPromise.resolve();}}这证明系统可以调度扩展能力但不能证明照片、Preferences 和文档索引已经被正确恢复。完成标准必须下沉到数据验证。二、列出需要保护的数据清单趣味相机至少有四类数据数据例子丢失影响推荐策略用户照片文件拍照结果、导出图核心内容丢失明确文件或媒体库归属照片元数据标题、水印、分辨率、来源相册列表不完整Preferences 备份与迁移文档索引photoId、摘要、页数文档工作台丢失Preferences 备份与重建临时预览数据PixelMap、倒计时、当前 Tab无长期价值不备份必须排除相机 Surface ID。PixelMap 实例。CameraInput、Session 和 Output。当前识别目标坐标。Timer 和 Busy 状态。页面弹窗是否打开。这些对象与当前进程和设备资源绑定恢复后重新创建才正确。三、照片元数据的当前存储模型相册服务使用 ArkData PreferencesconstPREF_NAME:stringwatermark_camera_album;constPREF_KEY_PHOTOS:stringcaptured_photos;privatestaticasyncinitInternal(context:common.UIAbilityContext):Promisevoid{try{PhotoAlbumService.prefsawaitpreferences.getPreferences(context,PREF_NAME);constraw:preferences.ValueTypePhotoAlbumService.prefs.getSync(PREF_KEY_PHOTOS,[]);if(typeofrawstring){PhotoAlbumService.cachedPhotosPhotoAlbumService.parsePhotos(raw);}}catch(error){PhotoAlbumService.cachedPhotos[];}}写入流程awaitprefs.put(PREF_KEY_PHOTOS,JSON.stringify(cachedPhotos));awaitprefs.flush();Preferences 中保存 JSON 字符串恢复后必须执行 JSON 解析、默认值补齐和条数限制而不是直接断言为当前CapturedPhoto[]。四、文档索引必须与照片建立可检查关系文档服务单独使用constPREF_NAME:stringwatermark_camera_documents;constPREF_KEY_DOCUMENTS:stringconverted_documents;文档模型包含photoIdexportinterfaceCapturedDocument{id:string;photoId:string;title:string;createdAt:string;sourcePhotoTitle:string;sourceCreatedAt:string;pageCount:number;documentType:imageDocument;status:ready;summary:string;captureSummary:string;watermark?:WatermarkSnapshot;}恢复后要检查引用functionfindBrokenDocuments(photos:CapturedPhoto[],documents:CapturedDocument[]):CapturedDocument[]{constphotoIdsnewSet(photos.map((photo:CapturedPhoto)photo.id));returndocuments.filter((document:CapturedDocument){return!photoIds.has(document.photoId);});}当前CapturedDocument还保存来源标题和水印快照因此即使源照片记录缺失也可以显示部分信息。但页面应标记“源照片不可用”不能继续提供打开原图操作。五、给持久化数据加入schemaVersion当前 Preferences 直接保存数组。更可维护的格式interfaceAlbumStoreV2{schemaVersion:2;updatedAt:string;photos:CapturedPhoto[];}interfaceDocumentStoreV2{schemaVersion:2;updatedAt:string;documents:CapturedDocument[];}读取时兼容旧数组functiondecodeAlbum(raw:string):AlbumStoreV2{constparsed:ObjectJSON.parse(raw)asObject;if(Array.isArray(parsed)){return{schemaVersion:2,updatedAt:newDate().toISOString(),photos:migratePhotosV1(parsedasCapturedPhoto[])};}conststoreparsedasAlbumStoreV2;returnmigrateAlbumStore(store);}schemaVersion表示数据结构版本不等同于应用versionCode。一个应用版本可能不改存储结构也可能在开发期多次升级结构。六、恢复回调中的BundleVersion用于选择迁移路径onRestore(bundleVersion)提供来源版本信息。可把它转换为迁移上下文interfaceRestoreContext{sourceVersionCode:number;sourceVersionName:string;targetSchemaVersion:number;}迁移策略示例来源 versionCode 1000002 - captureSource 缺失时设为 simulated 来源 versionCode 1000003 - resolutionLabel 缺失时设为 12MP (4:3) 来源 versionCode 1000004 - watermark 缺失时保持 undefined 所有来源版本 - 数值范围夹取 - 状态值归一化 - 删除非法空 ID不要用版本号代替字段检查。用户可能跨多个版本恢复也可能有部分写入数据。迁移函数应同时检查来源版本和实际字段。七、模型迁移必须是幂等的迁移可能在系统恢复、应用升级和下一次冷启动中重复执行。执行两次的结果必须与一次相同。照片规范化functionnormalizePhoto(photo:CapturedPhoto):CapturedPhoto{return{id:photo.id,title:photo.title||未命名照片,createdAt:photo.createdAt||未知时间,layerCount:safeNumber(photo.layerCount,0),layerSummary:photo.layerSummary||未添加水印,filterName:photo.filterName||无滤镜,filterIntensity:safePercent(photo.filterIntensity),frameName:photo.frameName||无相框,beautySummary:photo.beautySummary||标准模式,beautyFeature:photo.beautyFeature||标准,beautyIntensity:safePercent(photo.beautyIntensity),resolutionLabel:photo.resolutionLabel||12MP (4:3),captureSource:photo.captureSource||simulated,captureSummary:safeSummary(photo.captureSummary),status:photo.statussaved?saved:preview,watermark:cloneWatermark(photo.watermark)};}规范化函数不能生成新的随机 ID 或当前时间否则每次运行都会改变结果。八、恢复后的缓存必须重新加载两个服务都缓存数据privatestaticprefs:preferences.Preferences|nullnull;privatestaticinitTask:Promisevoid|nullnull;privatestaticcachedPhotos:CapturedPhoto[][];如果进程在恢复期间已存在磁盘 Preferences 被替换后内存缓存可能仍是旧值。可以提供显式重载staticasyncreload(context:common.UIAbilityContext):Promisevoid{PhotoAlbumService.prefsnull;PhotoAlbumService.initTasknull;PhotoAlbumService.cachedPhotos[];awaitPhotoAlbumService.init(context);}更安全的方式是把初始化状态封装成实例并在恢复完成后发布一个内部事件页面收到后重新执行listPhotos()与listDocuments()。九、初始化任务要避免竞态当前initTask使多个调用者共享同一个初始化 Promisestaticinit(context:common.UIAbilityContext):Promisevoid{if(PhotoAlbumService.initTask!null){returnPhotoAlbumService.initTask;}PhotoAlbumService.initTaskPhotoAlbumService.initInternal(context);returnPhotoAlbumService.initTask;}页面的listPhotos()会等待privatestaticasyncwaitForInit():Promisevoid{if(PhotoAlbumService.initTask!null){awaitPhotoAlbumService.initTask;}}这里还有一个边界如果调用者在init()之前直接执行listPhotos()initTask为 null函数会立即返回空缓存。当前工程在EntryAbility.onCreate()先调用初始化因此正常路径成立测试和未来新 Ability 仍应显式保证顺序或让服务保存 Context 后延迟初始化。十、恢复前先定义冲突策略设备上可能已有新数据同时云端或迁移包带来旧数据。常见策略策略适用场景风险完全覆盖新设备首次恢复覆盖本地新增内容按 ID 合并同一账号多次迁移ID 冲突需定义胜负按时间取新有可靠更新时间旧格式可能无时间保留双方并重命名用户内容优先可能产生重复项本地相机更适合按稳定 ID 合并functionmergePhotos(local:CapturedPhoto[],restored:CapturedPhoto[]):CapturedPhoto[]{constmapnewMapstring,CapturedPhoto();restored.forEach(photomap.set(photo.id,normalizePhoto(photo)));local.forEach(photomap.set(photo.id,normalizePhoto(photo)));returnArray.from(map.values()).slice(0,60);}这里本地数据后写表示冲突时保留当前设备版本。实际产品也可以比较业务更新时间。十一、元数据恢复不能替代真实照片验证CapturedPhoto当前没有媒体 URI 字段更多承担业务快照角色。若应用未来保存真实文件必须增加interfaceMediaReference{uri:string;mediaId?:string;contentHash?:string;byteSize?:number;}恢复后逐项验证URI 是否可访问 - 文件大小是否合理 - 可否创建 ImageSource - 图片宽高是否有效 - 哈希是否匹配如果保存不要把设备绝对路径当作跨设备稳定标识。媒体库 URI、应用沙箱文件与系统备份策略的边界必须按目标 SDK 和发布环境真机验证。十二、失败恢复要保留原始数据迁移失败时不要立即用空数组覆盖 Preferences。推荐两阶段写入读取原始 JSON - 解析到临时对象 - 执行迁移 - 校验结果 - 写入临时 key - flush - 切换正式 key - 保留一次旧值备份可以使用constCURRENT_KEYcaptured_photos_v2;constBACKUP_KEYcaptured_photos_v1_backup;只有在新结构校验通过后才替换正式数据。下一版本稳定后再清理备份 key避免 Preferences 无限增长。十三、隐私与安全边界相机数据可能包含人脸、家庭环境、地点和水印备注。备份策略至少满足不把 PixelMap 转为日志或 Base64 文本。不在日志中输出完整 Preferences JSON。水印地点和备注不进入公开错误日志。只备份业务需要的字段。删除照片时同步处理关联文档与媒体文件。用户清除数据后不通过应用自建旁路自动恢复。发布前核对隐私说明中的备份和本地存储描述。当前代码使用%{public}s打印bundleVersion可以接受因为它是应用版本信息不要用同样方式打印照片元数据。十四、恢复结果报告内部可生成不含隐私的报告interfaceRestoreReport{sourceVersionCode:number;restoredPhotoCount:number;restoredDocumentCount:number;migratedPhotoCount:number;brokenDocumentCount:number;missingMediaCount:number;invalidRecordCount:number;durationMs:number;}报告用于测试日志或聚合统计不包含标题、地点、备注、URI 和图片内容。十五、自动化测试设计准备固定夹具album_v1_array.json album_v2_store.json album_corrupted.json documents_v1_array.json documents_with_missing_photo.json测试断言旧数组能迁移到 V2 包装结构。缺失captureSource时补默认值。百分比字段夹取到 0 至 100。非法 JSON 返回错误不覆盖原存储。同一迁移执行两次结果相同。合并后照片不超过 60 条。合并后文档不超过 80 条。坏photoId能被识别并标记。Preferences 写入后已执行flush()。十六、真机备份恢复验收流程安装旧版本 - 拍摄并保存 3 张不同水印照片 - 转换 2 个文档 - 记录非隐私计数和 ID - 触发系统支持的备份流程 - 升级或换测试设备 - 触发恢复 - 启动 1.0.4 - 检查相册、文档、关联和媒体可访问性 - 再次冷启动确认结果稳定还要执行故障演练备份中途终止。Preferences JSON 截断。只恢复相册不恢复文档。文档引用不存在照片。设备已有一张新照片时恢复旧数据。来源版本跨越多个 schema。恢复后立即进入相册 Tab。十七、常见问题排查现象可能原因排查方式onRestore 有日志但列表为空只实现回调未验证数据读取 Preferences 并输出计数报告列表存在但图片打不开只恢复元数据检查媒体 URI 与文件策略文档显示但原照片缺失photoId引用断裂执行关联完整性检查恢复后仍显示旧缓存服务未 reload清理 initTask 并重新加载首次进入页面偶发空列表init 与 list 并发所有读取等待初始化 Promise迁移失败后数据全部消失直接覆盖正式 key使用临时 key 与两阶段提交多次恢复产生重复照片没有稳定 ID 合并按 ID 幂等合并新字段恢复后为 undefined缺少 schema 迁移集中执行 normalize 函数十八、上线前验收清单module.json5正确注册 backup extensionAbility。backup_config.json的总开关符合产品策略。已列出照片文件、照片元数据和文档索引的数据边界。临时 Surface、PixelMap 和相机会话不参与备份。持久化结构包含独立schemaVersion。BundleVersion只用于选择迁移路径不替代字段校验。迁移函数可重复执行且结果幂等。恢复后会刷新服务缓存和页面状态。相册和文档按稳定 ID 合并。文档的photoId引用经过完整性检查。真实媒体 URI 或文件可访问性经过真机验证。失败迁移不会覆盖原始数据。日志和报告不包含图像、地点、备注或 URI。已完成跨版本、坏数据和部分恢复演练。总结allowToBackupRestore: true和两个回调只是备份恢复的入口不是完成证明。真正的工程闭环包括数据清单、Preferences 结构版本、幂等迁移、缓存重载、照片与文档引用校验、真实媒体可访问性和失败回滚。对趣味相机而言恢复成功的标准不是onRestore被调用而是用户重新打开应用后照片、文档、水印信息和可操作状态一致并且多次冷启动不会再次改变数据。把恢复流程做成可验证的数据迁移才能让系统能力真正保护用户内容。