【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 等,而不是代码中写的 id、timestamp。

一句话:@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)
}

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

未踩的坑预告

项目中另一个模型也用了 @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

相关推荐
摇滚侠16 小时前
《On Java 中文版 基础卷》阅读笔记 对象无处不在 03
java·笔记·python
知产xiao_xin16 小时前
地理标志权
经验分享·笔记·知识产权
Answer1st17 小时前
【嵌入式学习】嵌入式原理知识-定时器(六)
学习
\光辉岁月/18 小时前
1.mybatis学习-基础
学习·mybatis
摆烂z18 小时前
多模态大模型微调笔记
笔记
胡二拉二胡18 小时前
抗遗忘单词表深度评测:FSRS 算法与学练考闭环实测
经验分享·笔记
へ蟲児.18 小时前
2026三大自带完整体系的英语学习App深度测评:哪款更适合儿童启蒙?
学习
用户140360581938319 小时前
Phase A · Step 2:预训练权重与 Pipeline 验证准备
笔记
苦猿的大模型日记19 小时前
Day64|从0学习 Claude Code(十四):MCP,给 Agent 装个工具插座
学习