一个问题:
每次从服务器取到数据后,都是调用adapter.notifyDataSetChanged();
进行刷新。那局部刷新(adapter.notifyItemChanged();
)的这些东西不是白瞎了吗?对性能也不好,还没有动画。
怎么办:
用DiffUtil吧!号称可以进行局部刷新神器,让你的item 该刷新的地方就刷新,数据没有改变的地方不刷新(DiffUtil 内部调用了的局部刷新,还支持item动画哟!)。
怎么用:
- 重写一个类:DiffUtil.Callback ,自己写,(注意打Log, 如果觉得自己菜的话)
public class MyDiffCallback extends DiffUtil.Callback {
//Thing 是adapter 的数据类,要换成自己的adapter 数据类
private List current;
private List next;
public MyDiffCallback(List current, List next) {
this.current = current;
this.next = next;
Log.d("数据c", current.toString());
Log.d("数据n", next.toString());
}
/**
* 旧数据的size
*/
@Override
public int getOldListSize() {
return current.size();
}
/**
* 新数据的size
*/
@Override
public int getNewListSize() {
return next.size();
}
/**
* 这个方法自由定制 ,
* 在对比数据的时候会被调用
* 返回 true 被判断为同一个item
*/
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
Thing currentItem = current.get(oldItemPosition);
Thing nextItem = next.get(newItemPosition);
return currentItem.getId() == nextItem.getId();
}
/**
*在上面的方法返回true 时,
* 这个方法才会被diff 调用
* 返回true 就证明内容相同
*/
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
Thing currentItem = current.get(oldItemPosition);
Thing nextItem = next.get(newItemPosition);
return currentItem.equals(nextItem);
}
}
- 并创建它,
MyDiffCallback callback = new MyDiffCallback(adapter.things, things);
对比数据
DiffUtil.DiffResult result = DiffUtil.calculateDiff(callback);
- 然后刷新,完事儿
result.dispatchUpdatesTo(adapter);
源码分析,使用请回:
使用起来,尤其面临大量数据刷新时,你会感到从所未有的高效和简洁但是呢?
why are you so niu bi ?
read the fucking source code
想了解原理只有阅读源码。阅读源码时,有一点讲究,不是拿着一个类直接阅读,而是揪其一点不断深入。
比如他是怎么对比的:
一般我看不懂英文,我会先点开这个方法,看见这个方法内部调用了一个方法:
点入
new AdapterListUpdateCallback(adapter)
意外收获
发现有一个adapter 传入这个对象,看源码它把adapter 包了一层,实现了接口,调用了adapter局部刷新的方法。
难道dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));
里面通过这个包裹实现了局部刷新的?
public final class AdapterListUpdateCallback implements ListUpdateCallback {
@NonNull
private final RecyclerView.Adapter mAdapter;
/**
* Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter.
*
* @param adapter The Adapter to send updates to.
*/
public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
mAdapter = adapter;
}
/** {@inheritDoc} */
@Override
public void onInserted(int position, int count) {
mAdapter.notifyItemRangeInserted(position, count);
}
/** {@inheritDoc} */
@Override
public void onRemoved(int position, int count) {
mAdapter.notifyItemRangeRemoved(position, count);
}
/** {@inheritDoc} */
@Override
public void onMoved(int fromPosition, int toPosition) {
mAdapter.notifyItemMoved(fromPosition, toPosition);
}
/** {@inheritDoc} */
@Override
public void onChanged(int position, int count, Object payload) {
mAdapter.notifyItemRangeChanged(position, count, payload);
}
}
进入方法 dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));
public void dispatchUpdatesTo(ListUpdateCallback updateCallback) {
final BatchingListUpdateCallback batchingCallback;
if (updateCallback instanceof BatchingListUpdateCallback) {
//赋值
batchingCallback = (BatchingListUpdateCallback) updateCallback;
} else {
// 转换赋值
batchingCallback = new BatchingListUpdateCallback(updateCallback);
//noinspection UnusedAssignment
updateCallback = batchingCallback;
}
...
if (endX < posOld) {
//传入这个方法,发现其实在里面有调用局部刷新等方法
dispatchRemovals(postponedUpdates, batchingCallback, endX, posOld - endX, endX);
}
...
if (endY < posNew) {
//传入这个方法,发现其实在里面有调用局部刷新等方法
dispatchAdditions(postponedUpdates, batchingCallback, endX, posNew - endY,
endY);
}
...
for (int i = snakeSize - 1; i >= 0; i--) {
if ((mOldItemStatuses[snake.x + i] & FLAG_MASK) == FLAG_CHANGED) {
//这里也是
batchingCallback.onChanged(snake.x + i, 1,
mCallback.getChangePayload(snake.x + i, snake.y + i));
}
}
后面省略.....
batchingCallback.dispatchLastEvent();
}
我们发现BatchingListUpdateCallback
类中这个dispatchLastEvent()
方法中调用局部刷新,然后在 上面这个方法dispatchUpdatesTo(ListUpdateCallback updateCallback)
最后一行调用了dispatchLastEvent()
方法,代码中dispatchAdditions()
和dispatchRemovals()
方法中均有调用batchingCallback 的刷新方法,意外收获局部刷新的秘密!
回到正题,他到底是怎样高效对比数据的呢?
查看完整代码
public void dispatchUpdatesTo(ListUpdateCallback updateCallback) {
//跳过
final BatchingListUpdateCallback batchingCallback;
if (updateCallback instanceof BatchingListUpdateCallback) {
batchingCallback = (BatchingListUpdateCallback) updateCallback;
} else {
batchingCallback = new BatchingListUpdateCallback(updateCallback);
// replace updateCallback with a batching callback and override references to
// updateCallback so that we don't call it directly by mistake
//noinspection UnusedAssignment
updateCallback = batchingCallback;
}
//跳过
final List postponedUpdates = new ArrayList<>();
int posOld = mOldListSize;
int posNew = mNewListSize;
for (int snakeIndex = mSnakes.size() - 1; snakeIndex >= 0; snakeIndex--) {
//这是啥?好像是通过他的 x y 进行刷新的,具体不晓得了
final Snake snake = mSnakes.get(snakeIndex);
final int snakeSize = snake.size;
final int endX = snake.x + snakeSize;
final int endY = snake.y + snakeSize;
if (endX < posOld) {
//进入方法,一通操作,除了刷新,没有数据对比
dispatchRemovals(postponedUpdates, batchingCallback, endX, posOld - endX, endX);
}
if (endY < posNew) {
//进入方法,一通操作,除了刷新,没有数据对比
dispatchAdditions(postponedUpdates, batchingCallback, endX, posNew - endY,
endY);
}
for (int i = snakeSize - 1; i >= 0; i--) {
if ((mOldItemStatuses[snake.x + i] & FLAG_MASK) == FLAG_CHANGED) {
// 没有数据对比
batchingCallback.onChanged(snake.x + i, 1,
//进入方法,一通操作,除了刷新,没有数据对比
mCallback.getChangePayload(snake.x + i, snake.y + i));
}
}
posOld = snake.x;
posNew = snake.y;
}
batchingCallback.dispatchLastEvent();
}
看一遍你会得到上面一样结果,这时候会注意到自己找的地方错了,好像是数据对比在这之前,而且会注意到final Snake snake = mSnakes.get(snakeIndex);
这个对象,后面的代码是依照Snake 的x.y 等属性进行刷新,可以推断出mSnackes集合必然保存着对比数据后产生的结果,但是看它的注释一脸懵逼。
// The Myers' snakes. At this point, we only care about their diagonal sections.
// 迈尔斯的蛇。在这一点上,我们只关心它们的对角线部分。(对于这个集合的注释一脸懵逼)
private final List mSnakes;
通过这个集合,发现构造函数中对它进行的赋值:
DiffResult(Callback callback, List snakes, int[] oldItemStatuses,
int[] newItemStatuses, boolean detectMoves) {
//集合被赋值
mSnakes = snakes;
···
// 里面有add(snacke) 操作,但是看方法注释是针对于0 的情况,直接忽略。
addRootSnake();
// 这方法里面 调用了重写的areContentsTheSame(oldItemPos, newItemPos)(对比item内容是否有改变)
//,并把状态存入数组,用的时候直接取出。
findMatchingItems();
}
追踪构造函数(DiffResult())被调用之处是calculateDiff(Callback cb, boolean detectMoves)
发现calculateDiff(Callback cb, boolean detectMoves)
方法
被 DiffUtil.DiffResult result = DiffUtil.calculateDiff(callback);
调用(我们创建的时候,手动调的,请看前面使用部分文章)
数据如何对比依然是谜
推测知道snakes 集合是数据对比后的结果,它用来判断item是否直接刷新
追踪snakes集合产生过程必然就知道了对比过程
接着看caulateDiff(callback cb ,boolean deteMoves)
方法
public static DiffResult calculateDiff(Callback cb, boolean detectMoves) {
// 这里调用了我们使用时重写的方法,现在知道,重写的方法的用处了
final int oldSize = cb.getOldListSize();
final int newSize = cb.getNewListSize();
//结果集
final List snakes = new ArrayList<>();
···
// 一通操作
···
// 得到snake ,重要
final Snake snake = diffPartial(cb, range.oldListStart, range.oldListEnd,
range.newListStart, range.newListEnd, forward, backward, max);
if (snake != null) {
if (snake.size > 0) {
//添加到集合
snakes.add(snake);
}
//转成 x ,y 信息得到
snake.x += range.oldListStart;
snake.y += range.newListStart;
// 一通转化 和保存,返回数据结果集
····
return new DiffResult(cb, snakes, forward, backward, detectMoves);
}
发现diffPartial(cb, range.oldListStart, range.oldListEnd,range.newListStart, range.newListEnd, forward, backward, max);
方法,计算得snake 对象然后添加到集合中, 但是打开这个方法里面你会发现里面全是计算:
private static Snake diffPartial(Callback cb, int startOld, int endOld,
int startNew, int endNew, int[] forward, int[] backward, int kOffset) {
···
for (int d = 0; d <= dLimit; d++) {
for (int k = -d; k <= d; k += 2) {
···
while (x < oldSize && y < newSize
//这里调用了重写的方法耶,判断是否是同一个item
&& cb.areItemsTheSame(startOld + x, startNew + y)) {
x++;
y++;
}}}
for (int k = -d; k <= d; k += 2) {
for (int k = -d; k <= d; k += 2) {
···
while (x > 0 && y > 0
//这里调用了重写的方法耶,判断是否是同一个item
&& cb.areItemsTheSame(startOld + x - 1, startNew + y - 1)) {
x--;
y--;
}
}
}
···
}
不必担心,这里涉及一个算法就是Myers diff 算法(我打算单独分析),snakes集合目前的话可以理解为保存的对比结果就行了。
到这里diffUtil 的源码才算大概了解了。