【HarmonyOS学习笔记】2026-07-26 | @Trace 序列化 __ob_ 前缀陷阱与消息持久化

【HarmonyOS学习笔记】2026-07-26 | @Trace 序列化 _ob 前缀陷阱与消息持久化


date: 2026-07-26

tags: HarmonyOS, ArkTS, @Trace, @ObservedV2, JSON.stringify, toJSON, 持久化, fileIo, preferences
type: 踩坑实录

现象

消息列表持久化后,save() 日志显示写入成功,但 load() 返回 0 条。每次重启 App,界面上显示的都是硬编码的 Mock 数据,用户输入的消息全部丢失。

排查

load() 中加了一行日志打印第一条数据的 keys:

arkts 复制代码
if (i === 0) {
  hilog.info(this.logDomain, this.logTag,
    'load item0 keys: %{public}s', JSON.stringify(Object.keys(item)))
}

输出:

复制代码
load item0 keys: ["__ob_id","__ob_timestamp","__ob_sender","__ob_content","__ob_xp"]

所有字段前面被加了 __ob_ 前缀。 load() 中用 record['id'] 取值,自然取到 undefined,全部跳过,返回空数组。

根因

消息模型用了 @ObservedV2 + @Trace 装饰器:

arkts 复制代码
@ObservedV2
export class ChatMessage {
  @Trace id: string = ''
  @Trace timestamp: number = 0
  @Trace sender: string = 'user'
  @Trace content: string = ''
  @Trace xp: number = 0
}

@Trace 装饰器的工作原理是属性劫持 ------它把原始属性替换成带 __ob_ 前缀的内部属性,外部访问时通过 getter/setter 拦截以实现变化追踪。

JSON.stringify() 序列化时遍历对象自身可枚举属性,拿到的是被劫持后的内部键名 __ob_id__ob_timestamp 等,而不是代码中写的 idtimestamp

一句话:@Trace 为了观察属性变化劫持了属性名,JSON.stringify 看到的是劫持后的内部键名。

修复

1. ChatMessage 加 toJSON()

JSON.stringify() 发现对象有 toJSON() 方法时,会用其返回值替代对象本身进行序列化:

arkts 复制代码
@ObservedV2
export class ChatMessage {
  @Trace id: string = ''
  @Trace timestamp: number = 0
  @Trace sender: string = 'user'
  @Trace content: string = ''
  @Trace xp: number = 0

  static of(id: string, timestamp: number, sender: string, content: string, xp: number): ChatMessage {
    const msg = new ChatMessage()
    msg.id = id
    msg.timestamp = timestamp
    msg.sender = sender
    msg.content = content
    msg.xp = xp
    return msg
  }

  toJSON(): Record<string, Object> {
    return {
      'id': this.id as Object,
      'timestamp': this.timestamp as Object,
      'sender': this.sender as Object,
      'content': this.content as Object,
      'xp': this.xp as Object
    } as Record<string, Object>
  }
}

toJSON() 通过 this.id 等 getter 读取属性值(getter 返回的是正确的值),然后以正确的键名组装成普通对象返回。JSON.stringify 拿到这个普通对象,序列化出 "id" 而不是 "__ob_id"

2. MessageStore load 加 _ob 降级读取

已经存入磁盘的旧数据仍然是 __ob_ 前缀的键名,不能直接丢弃。load 时先尝试正常键名,取不到再降级读 __ob_ 前缀:

arkts 复制代码
let id = record['id']
let timestamp = record['timestamp']
let sender = record['sender']
let content = record['content']
let xp = record['xp']

if (id === undefined) { id = record['__ob_id'] }
if (timestamp === undefined) { timestamp = record['__ob_timestamp'] }
if (sender === undefined) { sender = record['__ob_sender'] }
if (content === undefined) { content = record['__ob_content'] }
if (xp === undefined) { xp = record['__ob_xp'] }

if (id !== undefined && timestamp !== undefined && sender !== undefined &&
  content !== undefined && xp !== undefined) {
  result.push(ChatMessage.of(
    id as string, timestamp as number, sender as string,
    content as string, xp as number
  ))
}

这样旧数据能正常加载,新数据(toJSON 修复后)也能正常加载。下次 save 时会以正确键名写回,逐渐完成数据迁移。

fileIo vs preferences:为什么选同步写入

消息持久化有两种方案可选:

维度 preferences fileIo
写入方式 put() + flush() writeTextSync()
flush 时效 异步,不保证立即写盘 同步,写入即生效
App 异常退出 flush 可能没执行完,数据丢失 已写入,不丢失
适合场景 偏好设置(丢了也不致命) 业务数据(丢了影响用户)

之前用 preferences 存消息,App 退出时 flush() 还没写完就进程终止了,消息丢失。换成 fileIo.writeTextSync() 后写入是同步的,代码执行完数据已在磁盘上。

arkts 复制代码
save(messages: ChatMessage[]): void {
  const json: string = JSON.stringify(messages)
  const file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.TRUNC)
  fs.writeSync(file.fd, json)
  fs.closeSync(file)
}

三条同步调用:openSyncwriteSynccloseSync,没有异步间隙,不怕进程意外终止。

未踩的坑预告

项目中另一个模型也用了 @ObservedV2 + @Trace

arkts 复制代码
@ObservedV2
export class TaskItem {
  @Trace id: string = ''
  @Trace name: string = ''
  @Trace category: string = ''
  @Trace xp: number = 0
  @Trace completed: boolean = false
  @Trace source: string = ''
}

没有 toJSON() 目前 TaskItem 没有持久化需求所以没踩到,但将来如果要 JSON.stringify 存盘,会一模一样地踩坑。

通用教训

陷阱 表现 防御
@Trace 劫持属性名 JSON.stringify 输出 __ob_ 前缀 加 toJSON() 返回正确键名
旧数据兼容 修复后已存盘的旧数据无法读取 load 时加 __ob_ 降级读取
flush 异步丢数据 App 退出时 flush 未完成 业务数据用 fileIo 同步写入
其他 @Trace 类 没 toJSON() 将来踩同样的坑 所有 @Trace 类统一加 toJSON()

一条规则:凡是用 @Trace 装饰的类,只要需要 JSON.stringify 序列化(持久化、网络传输、日志打印),就必须自定义 toJSON() 没有例外。

学习小结@Trace 装饰器通过属性劫持实现变化追踪,副作用是 JSON.stringify 输出 __ob_ 前缀键名而非原始键名。修复方案是自定义 toJSON() 返回正确键名的普通对象,并在 load 时加降级读取兼容旧数据。业务数据持久化选 fileIo 同步写入,避免 preferences.flush() 异步丢失。

懿路向前 · AI辅助整理

2026-07-26

相关推荐
Linux-lucky21 小时前
21-Linux学习之旅之HTTPS和安全加固
linux·运维·学习·ubuntu·https
wp123_121 小时前
硬件元器件笔记|IPX8 防水 Type‑C 母座安费诺 124018802112A 与 TONEVEE TY48086‑24A 分析
c语言·开发语言·笔记
mifengxing1 天前
操作系统 文件系统
笔记·操作系统·计算机408
学运维的Kysan1 天前
暑假运维学习打卡第二十六天8.17
学习
无聊的菜鸟1 天前
TMS320F2806X学习笔记(零)—— C2000、CCS、新建工程
笔记·mcu·c2000·f2806x
dear_bi_MyOnly1 天前
AI人工智能分类识别——机器如何学习
人工智能·学习·分类
ljt27249606611 天前
Compose笔记(八十三)--onVisibilityChanged
笔记
启雀AI1 天前
培训平台移动端离线学习方案设计与实现:视频缓存、断点续传与进度同步的工程实践
android·学习·缓存·音视频·企业lms
Fa_Mian_Tuan1 天前
图论基础|邻接矩阵超详细讲解(含无向/有向/带权图+完整可运行C语言代码)
c语言·数据结构·笔记·算法·图论
Bruce_Liuxiaowei1 天前
从零到可运行:基于 Vue3 + FastAPI + DeepSeek-V3 的 AI 英语单词学习系统全栈实战
人工智能·python·学习·fastapi·全栈·智能体