【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

相关推荐
hanlin031 小时前
刷题笔记:力扣第206题-反转链表
笔记·leetcode·链表
一只小菜鸡..1 小时前
南京大学 操作系统 (JYY) 学习笔记:可执行文件、链接器与 Shebang 的彩蛋
笔记·学习
高铭杰2 小时前
Golang sync.Mutex实现原理学习
java·学习·golang
狗凯之家源码网2 小时前
狗凯源码库学习资源真实价值评测
学习
Bernice橘子2 小时前
职场英语学习计划Day024
学习
奋发向前wcx10 小时前
y1,y2总复习笔记10 2026.7.24
笔记·哈希算法·散列表
MuMuMu122311 小时前
智慧监测的“火眼金睛”:当AIoT重构环保监管体系
笔记
Luu.ྀ12 小时前
React详细笔记
javascript·笔记·react.js
min(a,b)12 小时前
学习第 4 天:面向对象与异常处理
python·学习·学习方法