请坚持“单一事实来源”原则

一个"只读函数"引发的崩溃

今天排查到一个bug,崩溃栈指向一个只读函数。这个函数没有做任何写入,只是读取一下弹窗栈,判断当前有没有弹窗。

一个连内存都不写入的函数居然崩溃了,大概率是读取到了非法内存

定位问题

排查到最后,发现删除弹窗的逻辑是这样的:

1
2
3
if (node->state == POPUP) popStack.remove(node);
else                      normalList.remove(node);   // 兜底:当作普通节点
free(node);

看似合理,但问题在于:节点是**先入栈,再设置state**的。中间存在一个"已经入栈、但state还没被设置"的状态窗口。如果此时节点被删除,state != POPUP会让它走else分支,导致它不会从popStack中被正确移走。下次遍历popStack时,就会访问到已经free的野指针而崩溃。

调整顺序就能解决吗?

那如果反过来,先设置好状态再入栈,问题是不是就解决了?

并没有。 这只是把不一致的窗口挪了个位置。问题的根因,是这个设计违反了一条重要准则:单一事实来源(Single Source of Truth)

根因:违反单一事实来源

一个节点属于哪个链表,本应是确定无误的事实。但代码里却存在两个说法

  • 一个是链表结构本身(节点实际挂在popStack还是normalList
  • 一个是被拿来"兼职"的**state字段**——它本来是用来表示节点状态的(如弹窗中、弹窗已完成),却被借用来推断节点归属

只要一个事实有两个来源,就一定存在它们不一致的窗口,而且迟早会有人忘记同步它们。

单一事实来源:一个事实只能有一个权威来源,其余信息都从它推导,而不是另存一份。

修复方向:把信息收敛到唯一来源

方案一:用的时候实时查询,链表结构本身就是唯一事实来源

1
2
3
if (popStack.contains(node))        popStack.remove(node);
else if (normalList.contains(node)) normalList.remove(node);
else assert(!"节点不在任何链表");   // 认不出就报警,而不是兜底猜

注意最后的assert:认不出归属就直接报警,而不是用else兜底"猜"一个分支——兜底猜正是最初bug的温床。

方案二:增加一个显式的owner字段,在挂链之前就设置好,让归属有一个明确、权威的来源。