一个"只读函数"引发的崩溃
今天排查到一个bug,崩溃栈指向一个只读函数。这个函数没有做任何写入,只是读取一下弹窗栈,判断当前有没有弹窗。
一个连内存都不写入的函数居然崩溃了,大概率是读取到了非法内存。
定位问题
排查到最后,发现删除弹窗的逻辑是这样的:
| |
看似合理,但问题在于:节点是**先入栈,再设置state**的。中间存在一个"已经入栈、但state还没被设置"的状态窗口。如果此时节点被删除,state != POPUP会让它走else分支,导致它不会从popStack中被正确移走。下次遍历popStack时,就会访问到已经free的野指针而崩溃。
调整顺序就能解决吗?
那如果反过来,先设置好状态再入栈,问题是不是就解决了?
并没有。 这只是把不一致的窗口挪了个位置。问题的根因,是这个设计违反了一条重要准则:单一事实来源(Single Source of Truth)。
根因:违反单一事实来源
一个节点属于哪个链表,本应是确定无误的事实。但代码里却存在两个说法:
- 一个是链表结构本身(节点实际挂在
popStack还是normalList) - 一个是被拿来"兼职"的**
state字段**——它本来是用来表示节点状态的(如弹窗中、弹窗已完成),却被借用来推断节点归属
只要一个事实有两个来源,就一定存在它们不一致的窗口,而且迟早会有人忘记同步它们。
单一事实来源:一个事实只能有一个权威来源,其余信息都从它推导,而不是另存一份。
修复方向:把信息收敛到唯一来源
方案一:用的时候实时查询,链表结构本身就是唯一事实来源
| |
注意最后的assert:认不出归属就直接报警,而不是用else兜底"猜"一个分支——兜底猜正是最初bug的温床。
方案二:增加一个显式的owner字段,在挂链之前就设置好,让归属有一个明确、权威的来源。