所有异步加载数据的元素搭配全量刷新,在未加载完毕前会展示其他节点的旧信息;即使刷新时重置视图也无法解决,只是从A->A->B改成A->空->B,依然存在明显的跳变。
QQ 9 采用的"增量刷新"就能很好的解决上述两个体验问题。此外,还有一个全量刷新无法实现的隐藏福利:节点动画,如下视频所示。
,时长00:21
实现增量刷新需要有个可靠的 Diff 算法,告知系统有变化的节点是需要执行刷新、插入、删除、移动中的哪种操作,一旦给到错误的信息将会直接导致 App Cras。敲定算法过程也是一波三折。
首先,阅读源码发现 Android 与 iOS 系统内置的 Diff 工具都是采用 Myers 算法实现的。
Myers:计算结果保存在changes的数组内,其中只有insert、remove两种类型。(来源:Swift Diffing)
Myers算法求解过程,通过插入、删除求源到目的的最短编辑距离。来源:AnO(ND) difference algorithm and its variations
该算法在计算移动时存在"缺陷",其通过插入 删除行为推测移动,特定场景下移动操作会降级为插入 删除。比如,先删除再移动就会转换为删除 插入,反之则是移动 删除:
- 删 移 → 删 增:
- 数据集A:[1, 2, 3, 4, 5]->数据集B:[2, 3, 5, 4]。会删除1、4,接着插入4。
- 移 删 → 移 删:
- 数据集A:[1, 2, 3, 4, 5]->数据集B:[1, 2, 4, 3]。会交换3、4,随后删除5。
经过分析,理想的 Diff 算法应该具有以下两种特质:
- 能够记录节点之间的移动关系,并不是通过插入、删除的联系推断移动。
- 具备较低的时间复杂度与空间复杂度。
对比行业方案后,选中论文《A technique for isolating differences between files》中描述的 Heckel Diff 算法。该算法的最优、平均、最差复时间/空间复杂度均为 O(m n),优于 Myers 算法的 O((m n)*d)。其符号表的实现方式保证所有移动操作均被记录,不会再出现 Myers 中丢移动操作的情况,如下图所示。
Heckel算法通过6个步骤借助符号表产生新老数据之间的Diff信息
- PASS1. 建立新数据所需新索引数组(NA)与 Symbol Table 之间的关系
- PASS2. 建立老数据所需旧索引数组(OA)与 Symbal Table 之间的关系。
- PASS3. 查找位置没有变化的节点,更新新旧索引数组(NA、OA)中的索引信息。
- PASS4 - PASS5:适用于对两个本文进行比较的 Case(存在 Key 值相同的情况),在 QQ 的应用场景中不允许出现相同 Key 值的情况,可跳过。感兴趣的同学可以直接查阅论文。
- PASS6. 根据现有结果计算差异,如图下图所示:
D表示被删除,U表示没有变化,4、5之间存在移动关系。
那么 Heckel 算法是完美的吗?不然,它并没有考虑冗余的移动信息,冗余的移动操作会导致下图中的动画错乱问题。