
本地应用最容易出现一种“看起来已经成功”的数据故障用户点击收藏图标立刻亮了返回收藏页却没有这条数据。删除笔记后列表立即消失重新进入页面又恢复。连续点击两次收藏最终状态与最后一次操作相反。应用启动早期写入学习时长界面统计发生变化但重启后数据归零。这些问题通常不是 Preferences API 不会用而是 UI 状态、进程内状态和磁盘状态没有明确提交边界。页面先修改State存储方法返回Promisevoid内部再用空catch吞掉失败于是调用方无法知道“内存变了但磁盘没有变”。多个页面再靠aboutToAppear()或onPageShow()各自重读就会形成短暂但真实的不一致。本文基于化学实验室 1.0.5的真实 HarmonyOS ArkTS 源码展开。DataStore.ets使用kit.ArkData的 Preferences 保存收藏、实验记录、笔记、答题记录和学习统计LabPage.ets在实验列表中增删收藏FavoritesPage.ets维护聚合收藏NotesPage.ets保存和删除笔记ReadingSpeechService.ets则保存当前朗读章节、播放状态和监听器等会话信息。当前实现已经具备数据封装意识但仍有四个可以从源码复核的问题写失败被静默吞掉收藏同时维护两份 JSON 并重复写入读改写操作没有串行边界存在并发覆盖页面刷新依赖生命周期回调跨页返回时缺少统一变更信号。本文给出一套增量方案不替换现有 Preferences也不把所有状态塞进AppStorage。即时一致不是每个页面都重新读取一次而是只有落盘成功的状态变化才能发布统一修订号所有页面按同一修订号重建自己的可见快照。本文唯一校验标记CHEM-LAB-ONE12-PREFERENCES-COMMIT-CONSISTENCY-20260730本文会完成以下工程拆解还原DataStore的初始化、读取、写入和统计通知链路。区分持久状态、会话状态、页面状态和派生状态。把Promisevoid改成可判断成功与失败的提交结果。让初始化可等待避免“未就绪写入”静默丢失。用串行写队列保护收藏、笔记和记录的读改写。收敛双份收藏数据建立一次性迁移与唯一事实源。只在刷盘成功后发布全局修订号。让页面支持加载、保存、失败、回滚和重试状态。建立并发、重启、损坏数据和页面返回测试矩阵。一、当前 DataStore 保存了哪些数据DataStore.ets的 Preferences 文件名为const PREF_NAME physics_app_data虽然应用已经是化学实验室文件名仍保留旧项目语义。文件名本身不会导致运行错误但它是迁移历史的信号存储层同时兼容旧收藏键、旧学习分钟键和新的聚合收藏键。当前主要键包括数据域键存储形态实验收藏旧数据favorite_experiments实验 ID 数组 JSON聚合收藏all_favoritesFavoriteItem[]JSON实验记录experiment_records记录数组 JSON用户笔记user_notes笔记数组 JSON模拟实验次数experiment_countnumber学习时长learning_secondsnumber旧学习时长learning_minutesnumber中考练习记录exam_practice_records记录数组 JSON这些数据适合轻量 Preferences但数组 JSON 的读改写必须特别注意并发和失败结果。二、当前初始化失败会被隐藏现有初始化代码为static async init(context: common.UIAbilityContext): Promisevoid { try { DataStore.prefInstance await preferences.getPreferences(context, PREF_NAME) await DataStore.refreshStatsSnapshot() } catch (_) { DataStore.prefInstance null } }调用方只能等待一个Promisevoid无法区分成功和失败。更麻烦的是初始化异常已经在内部被捕获外层.then()仍可能被执行。应用日志会表现为“初始化完成”但prefInstance实际为null。这不是日志措辞问题而是后续所有写操作的前置条件失真。存储层需要公开明确的就绪结果。三、未初始化时不同写方法行为不一致putString()在实例为空时仍会更新派生统计但不会落盘if (!DataStore.prefInstance) { DataStore.notifyStatsChanged(key, value) return }putNumber()在实例为空时直接返回if (!DataStore.prefInstance) return因此同样是启动早期写入字符串统计可能在当前进程里变化数字统计完全不变两者都不会持久化。用户看到的内存状态与重启后的磁盘状态可能不同。正确的边界应是未就绪写入要么等待同一个初始化任务要么返回明确失败不能假装完成。四、空 catch 让页面无法确认提交结果当前字符串写入static async putString(key: string, value: string): Promisevoid { if (!DataStore.prefInstance) { DataStore.notifyStatsChanged(key, value) return } try { await DataStore.prefInstance.put(key, value) await DataStore.prefInstance.flush() DataStore.notifyStatsChanged(key, value) } catch (_) { } }无论put()、flush()还是序列化发生错误调用方的await都会正常结束。于是页面会认为保存成功继续关闭对话框、更新列表或返回上一级。存储层不能用void掩盖业务提交结果。应返回结构化结果export enum StoreErrorCode { NOT_READY not_ready, READ_FAILED read_failed, WRITE_FAILED write_failed, INVALID_DATA invalid_data } export interface StoreFailure { code: StoreErrorCode message: string } export interface StoreResultT { success: boolean value?: T error?: StoreFailure }页面只在success true时提交可见状态。五、先区分四类状态持久化设计的第一步不是选 API而是确定状态生命周期。1. 持久状态收藏、笔记、实验记录、答题结果和学习累计时长需要跨重启保留以 Preferences 为事实源。2. 会话状态ReadingSpeechService中的isSpeaking、currentSectionIndex、activeRequestId和引擎实例只在当前进程与当前朗读会话有效。进程被终止后不应自动恢复“正在朗读”。3. 页面状态列表加载中、保存中、删除确认、错误消息、编辑模式属于页面短生命周期由State管理。4. 派生状态收藏数量、实验次数、正确题数等可从规范数据计算。AppStorage适合承载轻量快照和修订号让多个页面及时响应但不应替代 Preferences 成为第二份数据库。四类状态边界清楚后才能避免“朗读状态被持久化”“收藏 JSON 又复制到全局状态”等错误扩散。六、让初始化任务幂等且可等待多个页面可能同时访问 DataStore。初始化不应重复打开 Preferences也不能让后来的调用绕过未完成任务。export class DataStore { private static prefInstance: preferences.Preferences | null null private static initTask?: PromiseStoreResultboolean static init(context: common.UIAbilityContext): PromiseStoreResultboolean { if (DataStore.prefInstance) { return Promise.resolve({ success: true, value: true }) } if (!DataStore.initTask) { DataStore.initTask DataStore.openPreferences(context) } return DataStore.initTask } private static async openPreferences( context: common.UIAbilityContext ): PromiseStoreResultboolean { try { DataStore.prefInstance await preferences.getPreferences( context, PREF_NAME ) await DataStore.runMigrations() await DataStore.refreshStatsSnapshot() return { success: true, value: true } } catch (error) { DataStore.prefInstance null return { success: false, error: { code: StoreErrorCode.NOT_READY, message: DataStore.errorMessage(error) } } } } }如果初始化失败结果会沿调用链返回。页面可以显示“本地数据暂不可用”而不是继续提供会静默丢失的保存按钮。七、所有读写都经过 Preferences 网关避免每个业务方法自己判断空实例可以集中取得已就绪实例private static getReadyPreferences(): StoreResultpreferences.Preferences { if (!DataStore.prefInstance) { return { success: false, error: { code: StoreErrorCode.NOT_READY, message: 本地存储尚未就绪 } } } return { success: true, value: DataStore.prefInstance } }字符串读取不再把所有异常伪装成默认值static async getString( key: string, defaultValue: string ): PromiseStoreResultstring { const ready DataStore.getReadyPreferences() if (!ready.value) { return { success: false, error: ready.error } } try { const value await ready.value.get(key, defaultValue) return { success: true, value: value as string } } catch (error) { return { success: false, error: { code: StoreErrorCode.READ_FAILED, message: DataStore.errorMessage(error) } } } }“键不存在”和“读取失败”是两种不同情况。前者可以使用默认值后者要保留错误证据。八、写成功的定义必须包含 flush一次对用户可见的保存至少包含三步写入内存、刷盘、发布变更通知。后两步不能颠倒。private static async commitString( pref: preferences.Preferences, key: string, value: string ): PromiseStoreResultstring { try { await pref.put(key, value) await pref.flush() DataStore.publishRevision(key) return { success: true, value: value } } catch (error) { return { success: false, error: { code: StoreErrorCode.WRITE_FAILED, message: DataStore.errorMessage(error) } } } }只有flush()完成后才增加修订号。否则其他页面可能收到通知重新读取的仍是旧数据。九、为什么数组 JSON 容易发生并发覆盖addFavorite()的流程是读取全部收藏、过滤同键项、插到数组头部、写回 JSON。removeFavorite()也执行读取、过滤、写回。如果两个操作并发操作 A 读取 [x] 操作 B 读取 [x] 操作 A 写入 [a, x] 操作 B 写入 [b, x] 最终 a 丢失Preferences 的单次put可以成功但业务级“读改写”并不自动具备事务性。需要在进程内为同一数据域建立串行队列。十、建立一个最小串行写队列对这个本地单进程应用可以先用 Promise 链保护业务读改写type StoreWriteTaskT () PromiseStoreResultT export class StoreWriteQueue { private tail: Promisevoid Promise.resolve() enqueueT(task: StoreWriteTaskT): PromiseStoreResultT { return new PromiseStoreResultT((resolve) { this.tail this.tail.then(async () { const result await task() resolve(result) }).catch(() { resolve({ success: false, error: { code: StoreErrorCode.WRITE_FAILED, message: 写入队列执行失败 } }) }) }) } }真实实现需要确保队列即使某个任务失败也能继续处理后续任务。收藏、笔记和记录可以按数据域分别建立队列避免互不相关的写入全部互相阻塞。十一、收藏必须选出唯一事实源当前收藏有两份持久数据favorite_experiments只保存实验 ID。all_favorites保存实验、知识、阅读和公式的完整条目。loadAllFavorites()会把旧实验 ID 合并到聚合收藏addFavorite()和removeFavorite()又同时维护两份数据。这种长期双写会增加失败组合第一份成功、第二份失败时两份状态立即分叉。更稳妥的做法是把all_favorites_v2定为唯一事实源。首次启动迁移旧favorite_experiments。迁移成功后写入favorites_schema_version 2。后续所有读写只操作all_favorites_v2。旧键保留一个版本周期用于回滚读取但不再双写。迁移必须幂等重复启动不能产生重复收藏。十二、收藏变更应该是一个原子业务操作收藏仓库可以把“读取、修改、写回”封装在同一队列任务里static toggleFavorite( item: FavoriteItem ): PromiseStoreResultboolean { return DataStore.favoriteQueue.enqueueboolean(async () { const loaded await DataStore.loadCanonicalFavorites() if (!loaded.success || !loaded.value) { return { success: false, error: loaded.error } } const key favoriteKey(item.type, item.id) const exists loaded.value.some((value: FavoriteItem) { return favoriteKey(value.type, value.id) key }) const next exists ? loaded.value.filter((value: FavoriteItem) { return favoriteKey(value.type, value.id) ! key }) : [item, ...loaded.value].slice(0, 120) const saved await DataStore.saveCanonicalFavorites(next) if (!saved.success) { return { success: false, error: saved.error } } return { success: true, value: !exists } }) }返回值同时表达提交是否成功和最终收藏状态页面不再自己猜测。十三、LabPage 当前存在重复写入LabPage.toggleFavorite()当前先调用removeFavorite()或addFavorite()随后又执行this.favoriteIds next await DataStore.saveFavorites(next)而addFavorite()内部已经会更新旧实验收藏列表removeFavorite()也会更新。页面再写一次不仅多余还可能与其他页面的收藏操作交错。改造后页面只调用一个仓库操作State favoriteSavingId: string State saveError: string private async toggleFavorite(exp: Experiment): Promisevoid { if (this.favoriteSavingId.length 0) { return } this.favoriteSavingId exp.id const result await DataStore.toggleFavorite({ id: exp.id, type: experiment, title: exp.name, subtitle: ${exp.category} · ${exp.description}, route: views/experiment/ExperimentSimPage, createdAt: Date.now() }) this.favoriteSavingId if (!result.success) { this.saveError result.error?.message ?? 收藏保存失败 return } await this.reloadFavorites() this.saveError }UI 只在存储确认后重新加载事实源避免双写和错误乐观状态。十四、悲观提交与乐观回滚怎么选本地 Preferences 通常很快收藏和删除操作优先选择悲观提交更简单按钮进入保存态刷盘成功后更新列表。用户几乎感受不到延迟却能避免失败回滚复杂度。如果产品要求立即响应可以采用乐观更新但必须保留旧快照const before this.notes.slice() this.notes next const result await DataStore.saveNotes(next) if (!result.success) { this.notes before this.pageError result.error?.message ?? 保存失败 }不能只做前半段乐观更新而没有回滚。十五、NotesPage 当前先改 UI 再保存添加笔记时this.notes [item, ...this.notes] await DataStore.saveNotesNoteItem(this.notes)删除笔记时this.notes this.notes.filter(n n.id ! id) await DataStore.saveNotesNoteItem(this.notes)由于saveNotes()内部吞异常页面永远不会回滚。正确做法是让保存返回结果并显式管理状态enum SaveState { IDLE idle, SAVING saving, FAILED failed } State saveState: SaveState SaveState.IDLE State saveMessage: string 提交成功才关闭编辑器或退出编辑模式失败时保留用户输入并显示重试按钮。十六、删除也必须等持久化成功再移除可见项删除收藏、笔记和实验记录都是不可忽视的数据操作。建议流程用户确认删除 - 标记目标为 deleting - 仓库串行读取最新快照 - 生成删除后的 next - put flush - 发布 revision - 页面用 next 或重新读取刷新若失败目标仍留在列表中并恢复删除按钮。这样页面不会出现“已经消失重进又回来”的错觉。十七、统一修订号只在提交成功后递增当前notifyStatsChanged()已经维护stats_version这是很好的方向但它只服务统计键且部分数据写入不会触发。可以升级为统一数据修订号const DATA_REVISION_KEY data_revision private static revision: number 0 private static publishRevision(domain: string): void { DataStore.revision AppStorage.setOrCreatenumber(DATA_REVISION_KEY, DataStore.revision) AppStorage.setOrCreatestring(last_changed_domain, domain) }data_revision只表示“规范数据已成功改变”不保存收藏和笔记正文。页面可以监听修订号决定是否重载。十八、页面不要只依赖 onPageShow 猜变化LabPage、NotesPage、FavoritesPage和ExperimentRecordsPage都在aboutToAppear()与onPageShow()中重载。页面生命周期重载仍然有价值但它不是跨组件数据同步协议尤其当主页面嵌在 Tabs 中时页面显示回调的触发方式需要实际验证。可以让相关页面订阅修订号StorageLink(data_revision) dataRevision: number 0 private loadedRevision: number -1 aboutToAppear(): void { this.reloadIfNeeded() } Watch(dataRevision) private onDataRevisionChanged(): void { this.reloadIfNeeded() } private async reloadIfNeeded(): Promisevoid { if (this.loadedRevision this.dataRevision) { return } await this.reload() this.loadedRevision this.dataRevision }具体装饰器写法应与项目当前状态管理版本保持一致。核心原则是刷新依据来自成功提交的修订号而不是每个页面自己推测其他页面是否改过数据。十九、防止旧异步读取覆盖新页面状态页面连续触发两次reload()时第一次请求可能后完成覆盖第二次的新数据。可以使用请求序号private loadRequestId: number 0 private async reload(): Promisevoid { const requestId this.loadRequestId this.pageState PageState.LOADING const result await DataStore.loadAllFavorites() if (requestId ! this.loadRequestId) { return } if (!result.success || !result.value) { this.pageState PageState.ERROR this.pageError result.error?.message ?? 读取收藏失败 return } this.favorites result.value.map((item: FavoriteItem) { return this.enrichFavorite(item) }) this.pageState result.value.length 0 ? PageState.EMPTY : PageState.CONTENT }这能避免页面返回、修订通知和手动重试同时触发时的旧结果回写。二十、JSON 解析失败不能一律当成空数组当前多个方法在JSON.parse()失败时返回[]。这会把数据损坏伪装成“用户从未保存过”。随后一次保存可能用空数组覆盖原始损坏数据失去恢复机会。建议区分三种状态键不存在返回正常空数组。JSON 合法但结构为空返回正常空数组。JSON 无法解析或字段不合法返回INVALID_DATA保留原字符串并记录迁移日志。对于重要数据可以在覆盖前保存一个有限数量的备份键例如all_favorites_backup_v2。不要无限累积备份。二十一、给 JSON 数据加 schemaVersion数组对象未来可能增加字段。可以使用包装结构export interface FavoriteEnvelope { schemaVersion: number updatedAt: number items: FavoriteItem[] }保存示例const envelope: FavoriteEnvelope { schemaVersion: 2, updatedAt: Date.now(), items: next }读取时先校验版本再执行明确迁移。这样应用更新后不会只能靠“字段可选”长期兼容所有历史结构。二十二、学习统计应从成功写入后派生当前notifyStatsChanged()会根据收藏 JSON、实验次数、学习秒数和答题记录更新 AppStorage 快照。这个方向可以保留但调用时机必须是刷盘成功之后。派生快照应满足可以从 Preferences 重新构建。丢失不会破坏原始数据。只在规范数据提交成功后更新。应用启动时做一次全量恢复。运行中用修订号做增量刷新。统计值不能比事实源先成功。二十三、ReadingSpeechService 为什么不该直接持久化ReadingSpeechService保存private static isSpeaking: boolean false private static currentSectionIndex: number -1 private static activeRequestId: string private static listener?: ReadingSpeechStateListener这些值与引擎实例、当前请求和页面监听器绑定属于会话态。若把isSpeakingtrue持久化应用重启时没有原引擎和有效请求恢复出的状态反而是假的。可以持久化的是用户明确需要的阅读信息例如字体大小、最近阅读文章 ID、阅读完成进度不应持久化正在播放、监听器和请求 ID。ReadingDetailPage已在aboutToDisappear()中执行ReadingSpeechService.setListener(undefined)这正是会话边界清理。存储设计应保留这种区分。二十四、错误信息要分层不暴露内部细节存储层返回技术错误码页面转换成用户可理解文案错误码日志信息用户提示NOT_READYPreferences 未初始化本地数据暂不可用请稍后重试READ_FAILED读取键与错误码数据读取失败可点击重试WRITE_FAILEDput/flush 阶段与错误码保存失败原内容已保留INVALID_DATAschema 与解析位置本地数据格式异常请勿继续覆盖生产日志记录键名、阶段、耗时和错误码不记录笔记正文或阅读内容。二十五、结构化日志让“偶发不同步”可复现一次写入至少记录operationIdfavorite-toggle-20260717-001 domainfavorites phaseflush previousRevision18 nextRevision19 durationMs11 resultsuccess失败时保留同一个 operationId。页面错误提示可以只显示简短编号开发者再从日志还原过程。二十六、页面状态模型要完整收藏和笔记页面至少需要export enum PageState { LOADING loading, EMPTY empty, CONTENT content, SAVING saving, ERROR error }重要交互还要有目标级状态例如deletingId、savingId。不能因为保存一条收藏就让整个页面永久不可用也不能在同一条记录保存中重复提交。二十七、持久化结构图结构中的职责边界如下Preferences Gateway只负责初始化、读、写、刷盘和技术错误。Repository负责收藏、笔记、记录的 schema、迁移和业务操作。Write Queue保护同一数据域的读改写顺序。Revision Bus只在提交成功后通知页面。Page Snapshot负责加载、保存、失败、回滚和重试。ReadingSpeechService独立保存会话态不进入持久仓库。二十八、最小改造顺序为了降低回归风险可以分八步落地为初始化和基础读写增加StoreResultT暂不改变业务键。去掉空 catch统一日志与错误码。增加按数据域串行的写队列。让收藏操作只从一个仓库方法提交。建立all_favorites_v2并迁移旧双份收藏。提交成功后发布data_revision。改造 Favorites、Lab、Notes 和 Records 页面状态。完成并发、重启、损坏数据和页面返回回归。每一步都应保留旧数据可读不能先删除旧键再验证迁移。二十九、纯函数与仓库测试优先测试不依赖 UI 的部分describe(FavoriteRepository, () { it(does not duplicate the same favorite, async () { // 给定同一 type id 连续添加两次 // 最终规范列表只保留一条 }) it(keeps the old snapshot when flush fails, async () { // 注入 flush 失败 // 返回 WRITE_FAILEDrevision 不增加 }) it(serializes concurrent toggles, async () { // 并发提交两次结果与队列顺序一致 }) })迁移测试至少覆盖空数据、仅旧实验收藏、已有聚合收藏、重复 ID、损坏 JSON 和重复执行迁移。三十、真机测试矩阵场景操作预期收藏保存实验室收藏一项后进入收藏页立即出现且重启仍存在收藏删除收藏页删除后返回实验室图标立即取消且重启不恢复连续切换快速点击同一收藏按钮 5 次无重复项最终状态可解释笔记新增保存后立即返回再进入内容存在、顺序正确笔记删除删除时模拟刷盘失败项目不消失显示重试记录删除删除最后一条实验记录空态和编辑状态同步冷启动早写数据初始化尚未完成时触发写入等待或明确失败不静默丢失数据损坏注入非法 JSON显示数据异常不覆盖原值朗读返回朗读中离开详情页监听器释放持久数据不受影响三十一、构建和运行验证完成类型与接口改造后先运行项目构建$env:DEVECO_SDK_HOMED:\huawei\DevEco Studio\sdk $env:PATHD:\huawei\DevEco Studio\jbr\bin; $env:PATH D:\huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat --no-daemon assembleHap构建通过后还要执行安装、启动、收藏、笔记、返回、重启和卸载烟测。持久化功能不能只依赖 Preview 验证因为真正的跨重启行为需要设备文件系统。三十二、故障排查顺序1. 页面变了重启后恢复旧数据检查页面是否在刷盘成功前更新了 UI查看提交结果与 flush 日志确认没有空 catch 把错误变成成功。2. 收藏页与实验室图标不一致检查是否仍在双写favorite_experiments和all_favorites确认两个页面都从规范收藏仓库读取并监听同一data_revision。3. 快速操作后丢一条数据检查读改写是否进入同一数据域队列。单次 put 成功并不能证明两个并发操作没有相互覆盖。4. 返回页面后仍显示旧列表检查修订号是否只在成功后发布页面是否记录了loadedRevision以及旧异步请求是否覆盖了新结果。5. 数据损坏后列表变空检查解析异常是否被转换成空数组并随后覆盖原值。损坏数据应进入错误态和迁移流程。三十三、改造前后对比维度改造前改造后初始化失败被捕获为 void幂等任务与明确结果写入空 catch无提交结果StoreResultT与错误码未就绪写静默返回或只改内存等待初始化或明确失败收藏数据两份 JSON 长期双写迁移后唯一事实源并发多个读改写可覆盖按数据域串行队列UI 更新常先改页面再保存成功后提交或失败回滚跨页刷新依赖页面生命周期猜测成功修订号驱动JSON 损坏当成空数组明确INVALID_DATA朗读状态与持久化边界未说明明确为会话态可观测性没有失败证据operationId、阶段和耗时三十四、验收清单[ ] DataStore 初始化结果可被调用方判断。[ ] 未就绪写入不会静默丢失。[ ]put或flush失败会返回WRITE_FAILED。[ ] 页面只在成功后更新或保留旧快照用于回滚。[ ] 收藏迁移后只有一个规范事实源。[ ] 同一收藏不会出现重复条目。[ ] 收藏、笔记和记录的读改写进入串行队列。[ ] 只有刷盘成功才增加data_revision。[ ] 返回相关页面后能按修订号即时刷新。[ ] 旧异步读取不能覆盖新状态。[ ] JSON 损坏不会被静默覆盖为空数组。[ ] schema 迁移可重复执行且不重复数据。[ ] 朗读引擎、监听器和请求 ID 不进入持久化。[ ] 错误日志不包含笔记正文等用户内容。[ ] 完成重启、并发、失败注入和页面返回测试。三十五、源码事实与建议实现的边界下面把DataStore.ets与ReadingSpeechService.ets中可以直接复核的事实与本文建议的持久化治理方案分开避免把设计提案误写成已经集成的功能。观察维度当前源码事实建议实现存储介质DataStore使用kit.ArkDataPreferences实例由静态字段持有保留 Preferences并通过窄接口暴露明确的就绪与提交结果初始化init获取 Preferences 并刷新统计异常被捕获后只把实例设为空使用幂等共享初始化任务结果区分ready/degraded/failed写入返回值putString、putNumber、putBoolean返回Promisevoid异常被空catch吞掉返回结构化CommitResult调用方只在提交成功后更新确认态未就绪写入putNumber与putBoolean在实例为空时直接返回putString还会先更新统计快照关键写入排队或明确失败禁止内存统计先于磁盘事实发布刷盘边界正常路径执行put flush后调用统计通知把一次业务提交、刷盘和修订号发布放进同一串行边界统计传播notifyStatsChanged更新AppStorage派生统计并递增进程内statsVersion统一为数据修订号只在可靠提交后发布页面按修订号重建快照收藏数据同时存在favorite_experiments与all_favorites统计取两者较大值迁移到一个规范收藏集合旧键只在迁移阶段读取读改写并发收藏、笔记等数组操作由异步读取后再写回当前没有同域串行队列按数据域串行化读改写避免快速操作的后写覆盖损坏数据多处 JSON 解析失败时回退为空集合或零错误原因不向调用方暴露区分空数据与损坏数据保留原值并提供修复或重试路径会话状态ReadingSpeechService的引擎、监听器、请求 ID 和章节索引仅在内存中继续保持会话态非持久化只持久化真正需要跨重启保留的数据本文提出的CommitResult、共享初始化任务、串行写队列、统一数据修订号、收藏迁移和页面保存状态都是建议实现当前源码尚未集成。本次精修没有执行 DevEco Studio 构建、Preferences 故障注入、并发点击、进程重启或跨页返回测试因此不能据此声称静默丢失、并发覆盖和页面不一致已经修复落地后必须用真实刷盘结果和重启数据复核。三十六、总结化学实验室已经使用 Preferences 把收藏、记录、笔记和学习统计集中进DataStore但“调用了保存方法”不等于“数据已经可靠提交”。当初始化失败、写异常和 JSON 解析异常都被空 catch 吞掉页面只能把任何await结束当作成功当收藏长期双写、数组读改写没有串行边界快速操作就可能覆盖当页面只依赖生命周期重读跨页返回的一致性也只能碰运气。可靠的本地状态链路应把一次业务操作定义为读取最新规范快照生成变更意图进入同域写队列完成put flush发布统一修订号最后让相关页面重建可见快照。失败时保留原数据、原页面状态和可重试入口。同时持久状态与会话状态必须分开。收藏和笔记需要跨重启保留朗读引擎、当前请求和监听器只属于本次会话。AppStorage可以传播修订号与派生统计但 Preferences 中的规范数据仍是事实源。做到这些以后保存、删除、返回和重启才真正指向同一个结果。参考资料Huawei DeveloperArkData 数据管理总览https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/data-mgmt-overviewHuawei Developer应用数据持久化与 Preferences 文档目录https://developer.huawei.com/consumer/cn/doc/Huawei DeveloperArkTS 状态管理概述https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-application-state-management-overview-V5项目源码entry/src/main/ets/utils/DataStore.ets项目源码entry/src/main/ets/pages/LabPage.ets项目源码entry/src/main/ets/views/mine/FavoritesPage.ets项目源码entry/src/main/ets/views/mine/NotesPage.ets项目源码entry/src/main/ets/utils/ReadingSpeechService.etsAI 辅助声明本文由 AI 辅助完成源码梳理、结构校对与配图设计技术结论均以文中列出的本地 ArkTS 源码为依据并由作者复核后发布。