HarmonyOS 7 Preferences + AppStorage:配置 Schema 升级中的分步迁移、失败回滚与多页面状态同步【鸿蒙心迹】
这次处理版本升级后的配置问题。
SettingsMigrationLab 在 3.1.x 里把字号保存成整数百分比,例如 100、115;到了 3.2.0,我想统一成 1.0、1.15,同时补上 theme、compactMode 等字段。
第一次实现时,我直接读旧值、覆盖新值。测试中途故意抛异常后,首页已经读到新字段,列表页仍拿着旧 AppStorage 值,Preferences 里也只改了一半。应用能启动,却处在“表面正常、配置已经分叉”的状态。
这篇只解决一个问题:Schema 从 2 升级到 3 时,怎样把迁移拆成可以验证、可以回滚,并且一次性同步到多个页面。

一、先给配置一个明确版本
最终数据固定为:cfg_20261001_11、3.2.0(30200)、Schema 2→3、MIGRATED、Migrated Keys 6、Rollback Count 1、Pages Synced 3、Theme system、Font Scale 1.15、Last Migration 10:54:32。
没有 schemaVersion 时,“某个字段有没有”不能说明用户从哪个版本升级上来,所以明确保存:
schemaVersion = 3
migrationState = MIGRATED
以后升级走显式版本路径。
二、迁移前先做快照
第一段代码解决中途失败。
import { preferences } from '@kit.ArkData'
private async createSnapshot(
prefs: preferences.Preferences,
snapshotId: string
): Promise<Record<string, preferences.ValueType>> {
const keys: string[] = [
'schemaVersion', 'theme',
'fontScalePct', 'fontScale',
'compactMode', 'migrationState'
]
const snapshot: Record<string, preferences.ValueType> = {}
for (const key of keys) {
snapshot[key] =
await prefs.get(key, '__MISSING__')
}
await prefs.put(
`snapshot:${snapshotId}`,
JSON.stringify(snapshot)
)
await prefs.flush()
return snapshot
}
我只保存这次会改到的字段。__MISSING__ 表示旧版本没有这个 key,回滚时应该删除。
三、迁移、验证、版本落盘必须有顺序
2→3 迁移里,schemaVersion 最后才写。
private async migrateV2ToV3(
prefs: preferences.Preferences
): Promise<void> {
await prefs.put('migrationState', 'MIGRATING')
await prefs.flush()
const oldPct =
Number(await prefs.get('fontScalePct', 100))
const fontScale =
Number((oldPct / 100).toFixed(2))
await prefs.put('fontScale', fontScale)
await prefs.put(
'theme',
await prefs.get('theme', 'system')
)
await prefs.put(
'compactMode',
await prefs.get('compactMode', false)
)
await prefs.delete('fontScalePct')
const theme =
String(await prefs.get('theme', 'system'))
if (fontScale < 0.8 || fontScale > 1.6) {
throw new Error(`invalid fontScale: ${fontScale}`)
}
if (!['system', 'light', 'dark'].includes(theme)) {
throw new Error(`invalid theme: ${theme}`)
}
await prefs.put('schemaVersion', 3)
await prefs.put('migrationState', 'MIGRATED')
await prefs.flush()
}
如果开头就把 schemaVersion 写成 3,后面失败,下次启动可能误以为升级结束。
本次字号 115 最终转成 1.15;主题没有用户值时使用 system;旧字段 fontScalePct 在验证通过后删除。Demo 第一次校验故意失败,因此最终 Rollback Count=1。
四、回滚要恢复旧值,也要恢复“原来没有”
我之前踩过的坑是:只把 fontScalePct 写回去,却忘了删除已经创建的 fontScale。下一次启动时两套字段同时存在,代码到底读哪个又成了新问题。
所以 restoreSnapshot() 会遍历快照:普通值用 put() 恢复,值为 __MISSING__ 的字段执行 delete();随后把 rollbackCount 加 1、把 migrationState 写成 ROLLED_BACK,最后统一 flush()。这一步的重点不是“恢复几个数字”,而是把整个 Schema 恢复到迁移前可解释的状态。

五、只有 MIGRATED 以后才同步 AppStorage
Preferences 是持久化配置,AppStorage 是页面运行态。迁移进行到一半时,不应该交替更新两边。
private async publishRuntimeState(
prefs: preferences.Preferences
): Promise<void> {
const theme =
String(await prefs.get('theme', 'system'))
const fontScale =
Number(await prefs.get('fontScale', 1.0))
AppStorage.setOrCreate<string>('theme', theme)
AppStorage.setOrCreate<number>('fontScale', fontScale)
AppStorage.setOrCreate<string>(
'settingsSchemaState', 'MIGRATED'
)
this.pagesSynced = 3
}
首页、列表页和“我的”页都订阅同一份运行态,最终三个页面统一为 theme=system、fontScale=1.15。迁移成功后由桥接层一次性发布,页面只订阅 AppStorage。
六、日志必须能复原迁移路径
工程中 EntryAbility 只负责启动迁移,SettingsMigrationService 负责版本判断、验证和回滚,SettingsStateBridge 只在成功后同步 AppStorage。
HiLog 固定保留:
schema 2 -> 3
snapshot=cfg_20261001_11
migratedKeys=6
rollbackCount=1
pagesSynced=3
State: MIGRATING -> MIGRATED
线上出现问题时,先看 schema 路径和 snapshotId。
七、运行结果要看页面是否收敛
最终手机界面显示:cfg_20261001_11、3.2.0(30200)、2→3、MIGRATED、Migrated Keys=6、Rollback Count=1、Pages Synced=3、Theme=system、Font Scale=1.15、Last Migration=10:54:32。
下方三个状态卡代表首页、列表页和“我的”页,都显示 SYNCED。
Schema 升级真正结束的条件,是持久层通过验证、运行态已发布、页面也完成收敛。

八、重复启动和跨版本升级必须幂等
schema 已经是 3 时直接退出,不能每次启动都重复创建快照。
如果用户从 schema 1 直接升级到 3,我会按 1→2→3 分步执行,每一步独立验证和回滚。
迁移中应用被终止时,下次启动根据 migrationState=MIGRATING 恢复快照或重新执行。
九、配置很轻,升级过程却不能靠运气
流程固定为:
SNAPSHOT → MIGRATING → VALIDATING → MIGRATED
失败路径:
MIGRATING → ERROR → ROLLED_BACK → RETRY
页面同步只发生在 MIGRATED 之后。
Preferences 不复杂,真正需要设计的是升级中的时间顺序。把持久化迁移和运行态发布拆开后,就不会因为某个页面先启动或一次异常而留下半套新数据。
更多推荐





所有评论(0)