一、DiffUtil 的算法原理
DiffUtil 使用 Myers 差分算法(基于 Eugene W. Myers 的《An O(ND) Difference Algorithm and Its Variations》)计算新旧数据集之间的最小编辑脚本(Minimum Edit Script),即从旧数据集转换为新数据集所需的最少操作(插入、删除、移动、更新)。以下是算法的核心原理:
- 基本概念
-
输入:
-
旧数据集(Old List)和新数据集(New List),由 DiffUtil.Callback 提供。
-
两个关键方法:
- areItemsTheSame(int oldItemPosition, int newItemPosition):判断两个位置的项是否代表同一实体(通常基于 ID)。
- areContentsTheSame(int oldItemPosition, int newItemPosition):判断两个项的内容是否相同(用于检测更新)。
-
-
输出:
- 一个差异集(DiffResult),包含插入、删除、移动和更新操作的序列。
-
目标:
- 找到最短编辑路径(Shortest Edit Script),即最少操作数将旧列表转换为新列表。
- 支持 RecyclerView 的局部刷新(如 notifyItemInserted()、notifyItemRemoved())和动画。
- Myers 差分算法原理
Myers 算法的核心是基于动态规划,寻找两个序列(旧列表和新列表)之间的 最长公共子序列(LCS, Longest Common Subsequence),并通过 LCS 推导出差异操作。以下是算法的关键步骤:
(1) 编辑距离与 LCS
-
编辑距离:将旧序列转换为新序列所需的最少插入和删除操作数。
-
LCS:旧序列和新序列的公共子序列,代表无需变更的项。
-
关系:
- 编辑距离 = 旧序列长度 + 新序列长度 - 2 × LCS 长度。
- 差异操作(插入、删除)由非 LCS 部分推导。
(2) 编辑图(Edit Graph)
-
Myers 算法将问题建模为一个编辑图:
-
X 轴表示旧序列(长度 N),Y 轴表示新序列(长度 M)。
-
每个格子 (x, y) 表示旧序列前 x 项和新序列前 y 项的比较状态。
-
路径:
- 水平移动:删除旧序列中的项(x → x+1)。
- 垂直移动:插入新序列中的项(y → y+1)。
- 对角移动:匹配相同项(x → x+1, y → y+1,无操作)。
-
目标:找到从 (0,0) 到 (N,M) 的最短路径(最少操作数)。
-
(3) Snake 路径
-
Snake:连续的对角移动,表示匹配的项(LCS 的一部分)。
-
D-path:编辑距离为 D 的路径,表示经过 D 次插入或删除到达某点 (x, y)。
-
算法核心:
- 通过动态规划计算所有可能的 D-path,记录每个路径的终点(称为“前沿点”,frontier)。
- 在每个 D 值(编辑距离)下,优先选择包含最多对角移动(即 LCS 更长)的路径。
- 当找到到达 (N,M) 的路径时,记录操作序列。
(4) 时间与空间复杂度
-
时间复杂度:O(ND),其中 N = max(旧序列长度, 新序列长度),D = 编辑距离。
- 最佳情况(序列完全相同):O(N)。
- 最差情况(完全不同):O(N²)。
-
空间复杂度:O(N) 或 O(D),通过优化只存储前沿点(frontier)。
二、DiffUtil 源码分析
以下基于 AndroidX 的 DiffUtil 源码(androidx.recyclerview.widget.DiffUtil),详细分析其实现细节。核心逻辑在 calculateDiff() 方法中,位于 DiffUtil.java。
- 核心方法:calculateDiff()
-
定义:
java
public static DiffResult calculateDiff(Callback cb, boolean detectMoves) { // 获取旧和新列表大小 final int oldSize = cb.getOldListSize(); final int newSize = cb.getNewListSize(); // 特殊情况处理 if (oldSize == 0 && newSize == 0) { return new DiffResult(cb, null, null, null); } // 执行 Myers 算法 return diffPartial(cb, 0, oldSize, 0, newSize, detectMoves); }
-
作用:
- 调用 diffPartial() 执行 Myers 算法,生成差异结果。
- detectMoves 参数控制是否检测移动操作(优化动画效果)。
- Myers 算法实现:diffPartial()
-
源码位置:DiffUtil.diffPartial()。
-
核心逻辑:
java
private static DiffResult diffPartial(Callback cb, int startOld, int endOld, int startNew, int endNew, boolean detectMoves) { final int oldSize = endOld - startOld; final int newSize = endNew - startNew; // 处理边界情况 if (oldSize == 0 && newSize == 0) { return new DiffResult(cb, null, null, null); } // 初始化前沿数组 final int max = oldSize + newSize + 1; final int[] forward = new int[max * 2 + 1]; final int[] backward = new int[max * 2 + 1]; // 执行 Myers 算法 List<Snake> snakes = new ArrayList<>(); // 正向搜索 computeSnakes(cb, startOld, endOld, startNew, endNew, forward, backward, snakes); // 处理移动操作(如果启用) List<Range> ranges = detectMoves ? computeMoves(cb, snakes, startOld, startNew) : null; return new DiffResult(cb, snakes, forward, backward, ranges); }
-
步骤解析:
-
初始化前沿数组:
- forward 和 backward 存储正向和反向搜索的 D-path 前沿点。
- 数组大小为 2 * (oldSize + newSize + 1),覆盖所有可能的 Δ(x - y)值。
-
正向搜索(computeSnakes):
- 从 (0,0) 开始,计算每一步的 D-path,记录 Snake(对角线段)。
- 使用动态规划,优先选择包含更多匹配项的路径。
-
反向搜索:
- 从 (N,M) 反向搜索,优化路径选择。
- 当正向和反向路径相遇时,生成完整的 Snake 序列。
-
移动检测:
- 如果 detectMoves=true,通过 computeMoves() 识别移动操作。
-
- Snake 数据结构
-
定义:
java
static class Snake { int startX, startY; // Snake 起点 int endX, endY; // Snake 终点 boolean reverse; // 是否为反向 Snake }
-
作用:
- 表示一段连续的匹配项(对角移动)。
- startX, startY 表示旧和新序列的起始位置,endX, endY 表示结束位置。
- 通过 Snake 序列,DiffUtil 推导出插入、删除和移动操作。
- 移动检测(computeMoves)
-
逻辑:
- 如果 detectMoves=true,DiffUtil 分析 Snake 序列,识别项的移动(如从位置 A 移到位置 B)。
- 通过 areItemsTheSame() 判断项是否为同一实体,标记为移动而非删除+插入。
-
源码:
java
private static List<Range> computeMoves(Callback cb, List<Snake> snakes, int startOld, int startNew) { List<Range> ranges = new ArrayList<>(); // 遍历 Snake,识别非匹配区域(插入、删除、移动) for (Snake snake : snakes) { // 处理 Snake 之间的间隙 int oldGap = snake.startX - prevSnake.endX; int newGap = snake.startY - prevSnake.endY; if (oldGap > 0 && newGap > 0 && cb.areItemsTheSame(prevSnake.endX, prevSnake.endY)) { ranges.add(new Range(prevSnake.endX, snake.startX, prevSnake.endY, snake.startY)); } } return ranges; }
-
优化效果:
- 移动操作减少不必要的删除+插入,提升动画流畅度(如 DefaultItemAnimator 的位移动画)。
- 结果分发:DiffResult
-
作用:
-
DiffResult 封装 Snake 序列和操作列表,通过 dispatchUpdatesTo(Adapter) 分发更新:
java
public void dispatchUpdatesTo(RecyclerView.Adapter adapter) { dispatchUpdatesTo(new AdapterListUpdateCallback(adapter)); }
-
转换为 notifyItemInserted()、notifyItemRemoved()、notifyItemMoved() 等操作。
-
三、DiffUtil 的优化手段
DiffUtil 的实现针对性能和实用性进行了多项优化,以下是主要优化点:
- 双向搜索(Bidirectional Search)
-
优化目标:减少搜索空间,提升算法效率。
-
实现:
- 结合正向(从 (0,0) 开始)和反向(从 (N,M) 开始)搜索。
- 当正向和反向路径相遇时,停止搜索,生成 Snake 序列。
-
源码:
java
while (k >= -d && k <= d) { if (forward[k - 1] < forward[k + 1]) { x = forward[k + 1]; } else { x = forward[k - 1] + 1; } // 检查是否与反向路径相遇 if (x >= backward[k - delta]) { // 生成 Snake } }
-
效果:
- 减少搜索时间,时间复杂度从 O(N²) 优化到 O(ND)。
- 适合大数据集(N 和 M 较大)。
- 差异最小化
-
优化目标:生成最少操作数,减少 RecyclerView 的刷新开销。
-
实现:
- Myers 算法优先选择包含最多对角移动(匹配项)的路径。
- 通过 areItemsTheSame() 和 areContentsTheSame() 精确区分移动和更新。
-
效果:
- 最小化 notifyItem*() 调用次数,减少 UI 重绘。
- 支持平滑动画(如 DefaultItemAnimator 的淡入淡出、移动效果)。
- 移动检测优化
-
优化目标:识别移动操作,优化动画效果。
-
实现:
- 当 detectMoves=true,通过 areItemsTheSame() 检测相同项的移动。
- 优先标记为移动而非删除+插入。
-
效果:
- 减少不必要的 ViewHolder 重建。
- 提升动画的视觉连贯性。
- 空间优化
-
优化目标:减少内存占用。
-
实现:
- 只存储前沿点(forward 和 backward 数组),而非整个编辑图。
- 数组大小为 O(N),避免 O(N²) 的空间复杂度。
-
效果:
- 适配低内存设备,处理大数据集时更高效。
- 异步支持
-
优化目标:避免主线程阻塞。
-
实现:
-
开发者可将 DiffUtil.calculateDiff() 放到后台线程:
java
AsyncTask.execute(() -> { DiffUtil.DiffResult result = DiffUtil.calculateDiff(callback); handler.post(() -> result.dispatchUpdatesTo(adapter)); });
-
-
效果:
- 避免 UI 线程卡顿,适合大数据集差异计算。
四、实际应用与注意事项
- 典型使用场景
-
列表更新:
-
当列表数据发生变化(如添加、删除、排序),使用 DiffUtil 计算差异:
java
DiffUtil.Callback callback = new DiffUtil.Callback() { @Override public int getOldListSize() { return oldList.size(); } @Override public int getNewListSize() { return newList.size(); } @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { return oldList.get(oldItemPosition).id == newList.get(newItemPosition).id; } @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { return oldList.get(oldItemPosition).equals(newList.get(newItemPosition)); } }; DiffUtil.DiffResult result = DiffUtil.calculateDiff(callback); result.dispatchUpdatesTo(adapter);
-
-
动画支持:
- 结合 DefaultItemAnimator,自动生成插入、删除、移动动画。
- 注意事项
-
areItemsTheSame() 优化:
- 使用唯一 ID(如数据库 ID)判断项是否相同,避免复杂比较。
- 错误实现可能导致不必要的更新或动画异常。
-
异步执行:
- 大数据集时,calculateDiff() 可能耗时,建议在后台线程运行。
-
移动检测:
- 启用 detectMoves=true 会增加计算开销,但优化动画效果。
- 如果动画不重要,可设为 false 提高性能。
- 与 ListView 的对比
- ListView 依赖 notifyDataSetChanged() 全量刷新,效率低且无动画支持。
- DiffUtil 提供局部刷新,结合 RecyclerView 的三层缓存和 ItemAnimator,显著提升性能和用户体验。
五、总结
DiffUtil 基于 Myers 差分算法,通过以下机制实现高效的列表更新:
-
核心算法:
- 使用编辑图和 Snake 路径计算最短编辑脚本,时间复杂度 O(ND)。
- 双向搜索优化搜索效率,空间复杂度 O(N)。
-
源码实现:
- calculateDiff() 和 diffPartial() 实现 Myers 算法,生成 Snake 序列。
- computeMoves() 检测移动操作,优化动画。
- DiffResult 分发更新到 Adapter,触发局部刷新。
-
优化手段:
- 双向搜索、差异最小化、移动检测、空间优化和异步支持。
- 适配大数据集、复杂动画和低内存设备。
-
实际效果:
- 减少 UI 重绘,提升更新效率。
- 支持平滑动画,增强用户体验。
DiffUtil 的设计与 RecyclerView 的三层缓存、局部刷新和 ItemAnimator 深度整合,体现了 Android Framework 对性能和体验的极致追求。