一条错误结论在我的仓里被引用了七次,每处都标着「实证」——而那个实证从未发生过|HarmonyOS NEXT ArkTS 服务卡片 onAddForm 生命周期与 formId 持久化踩坑
首发于华为开发者论坛:https://developer.huawei.com/consumer/cn/blog/topic/03227528951187517
判断一条技术注释可不可信,别看它有没有"出处"——去找那个出处的原始记录。我最近拆了一条被七个工程引用、每处都写着「某工程实证」的结论,拆到最后发现——那个实证从来没有发生过。
事情出在 HarmonyOS NEXT 的桌面服务卡片上:FormExtensionAbility 的 onAddForm 生命周期,和 formId 的持久化。
那条注释写得有多像回事
它是这么说的:
onAddForm是同步返回的:它 return 之后扩展进程随时可能被回收,挂在额外 await 跳之后的写入就丢了。所以持久化动作要放进refresh()。
有机制、有因果、有修法,还有出处。七个工程就是这么一路抄下来的——我当时以为这条结论有实证背书,照做就行。
第一处不对劲来自官方文档。卡片生命周期回调的「使用限制」里写的是(可现查):
FormExtensionAbility 创建后 10 秒内无操作将会被清理,卡片进程退出,此时会触发 onStop 回调。
官方说的是 10 秒空闲超时,不是"随时回收"。两者差着几个数量级——做一次 await 写入绰绰有余。
第二处不对劲:官方那篇"主应用如何获取卡片添加/移除"的方案,推荐的做法恰恰是在 onAddForm/onRemoveForm 回调里对 formId 做持久化缓存、再让主应用跨进程读取。也就是说,官方让你写的位置,正是这条注释警告你别写的位置。
git show 父提交,是露馅的那一步
真正决定性的一步,是去看当年那次"修复"的父提交。那个被警告的"坏写法",在这个仓里从来就不存在。改动之前,refresh() 本来就是从 onAddForm fire-and-forget 调起的,只是压根没有 formId 登记代码。当时观察到的「formId 始终没有」,真因是没人写过它,不是"写了但丢了"。
更妙的是第四处:所谓的"修法",结构上什么都没改变。refresh() 同样是 fire-and-forget,登记同样位于一个 await 之后。它能工作的真实原因是重试——官方文档写明,点击添加至桌面会触发 onAddForm、onUpdateForm、onFormLocationChanged,而 onUpdateForm 也会调 refresh()。说白了,这条注释描述的因果从头到尾没成立过,代码碰巧能跑,靠的是另一条它没提的路径。
七次引用,没有一次是验证
这件事吓人的地方不在结论错了,在它的传播方式。「有出处」这件事本身,会关掉复核的冲动:一条注释写着「某工程实证」,读到的人默认那个实证发生过、并且就是这么做的。于是它被引用第二次、第三次——每一次引用都在增加它的可信度,而没有一次在增加它的证据。七次引用之后,它看起来像被七次验证过,实际上一次都没被验证过。
修的时候,我把原文留下了
代码一行没动。现状不是活缺陷,onUpdateForm 兜得住;发现推理错了不等于代码有缺陷,这两件事要分开判,否则会为了"改对"给已在架的版本引入回归风险。
注释必须改:把"放进 refresh() 就可靠"改成真实机制"靠 onUpdateForm 重试"。前一种说法留着,会让人在别处复制这个错误推理。新工程直接按官方来,在 onAddForm 里用同步 API 写。
处置上我做了个选择:保留原文、逐处标注,不直接删。这条传播链本身就是证据——删了,就看不出它是怎么扩散的了。
你可以在自己的仓里复现这一步
挑一条被多处引用的技术结论,三步:grep 出所有引用处,数一数;找到最早的那一处,git log 看它是哪次加的;然后 git show 那次提交的父提交,看"修改之前"到底是什么样。我这次就是第三步露的馅——前两步只会让你更相信它。
配两段可直接跑的:
# ① 那个"坏写法"到底存在过没有
git log -S "<注释关键词>" --oneline -- <文件>
git show <父提交>:<文件> | sed -n '1,60p'
# ② 这条结论被引用了多少处
# ⚠️ 用 os.walk 别用 grep:某些环境下 grep 会读 .gitignore,静默扫 0 个文件
import os
n = 0; tot = 0
for r, d, fs in os.walk('.'):
d[:] = [x for x in d if x not in ('.git', 'build', 'oh_modules', 'node_modules')]
for f in fs:
if not f.endswith(('.ets', '.md', '.json5')): continue
tot += 1
if '<关键词>' in open(os.path.join(r, f), encoding='utf-8', errors='ignore').read(): n += 1
print(f'{n} 个文件引用,扫了 {tot} 个')
第二段特意把分母打出来:报的是「引用 n 处,扫了 tot 个」。扫了几个文件必须一起亮出来,不然"零命中"和"零扫描"你分不出来。
这不是想说"注释里的结论都不可信"
绝大多数注释是对的。真正的判据就一句:看到「某某实证」四个字时,问一声"那个实证的记录在哪"。能指出一个复现工程、一条日志、一次提交的,是证据;能指出的东西是另一条注释的,是引用。
还得交代清楚推翻的强度。这回同一族里其实倒了两条,但倒的程度不一样:onAddForm 这条有官方文档加 git 考古双证;另一条(跨进程读首选项会被覆盖)手里就一个复现工程,且那个复现仅覆盖模拟器上的一种时序——所以我最多敢说"原文那套因果在该条件下不成立",不敢说"绝对安全"。
也别反向读成"官方文档一定对"。这篇拿官方文档推翻自家注释,是因为两者冲突时,官方描述的才是平台行为;但官方文档同样会滞后,最终判据永远是你在设备上看到的现象。
这是《鸿蒙开发踩坑实录》第 19 篇。引用它的人越多,它看着越可信,可一次引用也没让它变得更真
本文由作者与 AI 协作整理,事实经作者核验。
更多推荐

所有评论(0)