前言

ListAdapter发布很久了,但是一直没有机会使用,这次终于因为性能问题对项目进行优化,再次做一下笔记,相比传统的Adapter,它能够用较少的代码实现更多的RecylerView的动画,并且可以自动存储之前的list。并且ListAdapter还带有异步的DiffUtil的工具,只有当items变化的时候进行刷新,而不用刷新整个list,这无疑能大大提高RecyclerView的性能。

具体实现

在项目具体实现中,如果原先使用的是RecyclerView.Adapter, 则替换起来非常简单,只需要将继承改成ListAdapter即可。

public abstract class ListAdapter<T, VH extends RecyclerView.ViewHolder>
        extends RecyclerView.Adapter<VH>{
...
}

很明显,也只是多了一层继承(Android loves extends)。具体使用跟原来的实现差别并不大,不过多了些东西,下面列举一下。

构造方法变化

构造方法默认要实现DiffUtil.ItemCallback, 用于计算list的两个非空item的不同。具体要写两个抽象方法:

判断两个Objects 是否代表同一个item对象, 一般使用Bean的id比较

public abstract boolean areItemsTheSame(T oldItem, T newItem);
public abstract boolean areContentsTheSame(T oldItem, T newItem);

DiffUtil.ItemCallback的实现正是ListAdapter拓展类实现的精髓所在,它根据判断对象的一致性和对象的内容是否相同来进行UI的刷新和动画的实现,在性能方面,一般情况下当然是可以有所提升的。

提交数据集合

使用了ListAdapter, 相比RecyclerView.Adapter, 另一个好处就是不用构建内部的数据集合了,很简单直接adapter::submitList即可,而获取item,则使用getItem(position)即可。

//源码:提交list数据
public void submitList(List<T> list) {
	mHelper.submitList(list);
}

//源码:获取当前对象
protected T getItem(int position) {
	return mHelper.getCurrentList().get(position);
}

对于分页

数据分页我们当然要摒弃原来的办法,拥抱PagedListAdapter, 该方法比较类似ListAdapter, 唯一不同的就是提交数据的时候,提交的是PagedList(用于构建分页的类,对此不了解的话,可以查看相应的API)。

//源码:提交分页数据
public void submitList(PagedList<T> pagedList) {
	mDiffer.submitList(pagedList);
}

对于分页,要了解的东西其实还挺多的,这里不一一赘述。因为跟传统分页稍微有些不同,要适应起来要花点时间的。但是非常建议大家使用,无论是请求的分页,或者结合数据库使用(最好结合Room使用)。

源码部分剖析

对于一个新的类或工具,我们当然尽量不仅仅满足于使用,还要进来去深入了解其内部机理,这样才会在未来使用中遇到问题解决起来何以游刃有余。

对于ListAdapter, 本身不复杂, 其实我们更多的了解一下一个工具类AsyncListDiffer即可, 这是精髓所在。

我们会看到源码有这么一个东西——AsyncListDiffer,除了这个,整个类代码实现相当简洁。

//用于计算提交的两个lists的不同,在后台进行
private final AsyncListDiffer<T> mHelper;

每次adapter submit 数据的时候,该帮助类都会在后台计算, 并发送数据改变的信号(signal)。而在ListAdapter内封装有AsyncListDiffer的默认实现,而如果了解了该工具类的使用之后,我们完全可以撇开ListAdaper自己来封装一个Adapter(当然一般情况下我们还是建议使用ListAdapter, 除非有特殊要求,不然谁无聊去写这么多代码).

下面我们分析一下submit 数据的时候,该工具类做了什么事情呢?

public void submitList(final List<T> newList) {
	if (newList == mList) {
                //同样一个集合,当然不做改变,直接返回即可。
		return;
	}

        //每次提交数据,增加调度代, 因为是后台计算内容, 我们最后只要刷新当代内容即可。
	final int runGeneration = ++mMaxScheduledGeneration;

        //如果list为null,相当remove数据。
	if (newList == null) {
		int countRemoved = mList.size();
		mList = null;
		mReadOnlyList = Collections.emptyList();
		//刷新remove
		mUpdateCallback.onRemoved(0, countRemoved);
		return;
	}

	// 第一次插入数据
	if (mList == null) {
		mList = newList;
		mReadOnlyList = Collections.unmodifiableList(newList);
		mUpdateCallback.onInserted(0, newList.size());
		return;
	}

	final List<T> oldList = mList;
        //后台操作,使用Executor类
	mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
			@Override
			public void run() {
                        //计算不同
			final DiffUtil.DiffResult result = DiffUtil.calculateDiff(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 mConfig.getDiffCallback().areItemsTheSame(
							oldList.get(oldItemPosition), newList.get(newItemPosition));
					}

					@Override
					public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
					    return mConfig.getDiffCallback().areContentsTheSame(
							oldList.get(oldItemPosition), newList.get(newItemPosition));
					}

					@Nullable
						@Override
						public Object getChangePayload(int oldItemPosition, int newItemPosition) {
							return mConfig.getDiffCallback().getChangePayload(
									oldList.get(oldItemPosition), newList.get(newItemPosition));
						}
			});
                        //回到主线程刷新
			mConfig.getMainThreadExecutor().execute(new Runnable() {
					@Override
					public void run() {
					if (mMaxScheduledGeneration == runGeneration) {
					   latchList(newList, result);
					}
					}
					});
			}
	});
}

private void latchList(@NonNull List<T> newList, @NonNull DiffUtil.DiffResult diffResult) {
	mList = newList;
	// notify last, after list is updated
	mReadOnlyList = Collections.unmodifiableList(newList);
	diffResult.dispatchUpdatesTo(mUpdateCallback);
}

提交数据的过程代码看起来虽然很多,但是基本就是几步:

对list进行初始判断(是否新数据,是否为空) 后台计算list是否相同 回主线程刷新数据 那么,它是怎么计算list的不同的呢?我们看看这段代码:

//detectMoves 表示是否检测item 移动
public static DiffResult calculateDiff(Callback cb, boolean detectMoves) {
	final int oldSize = cb.getOldListSize();
	final int newSize = cb.getNewListSize();

	final List<Snake> snakes = new ArrayList<>();

	// instead of a recursive implementation, we keep our own stack to avoid potential stack
	// overflow exceptions
	final List<Range> stack = new ArrayList<>();

	stack.add(new Range(0, oldSize, 0, newSize));

	final int max = oldSize + newSize + Math.abs(oldSize - newSize);
	// allocate forward and backward k-lines. K lines are diagonal lines in the matrix. (see the
	// paper for details)
	// These arrays lines keep the max reachable position for each k-line.
	final int[] forward = new int[max * 2];
	final int[] backward = new int[max * 2];

	// We pool the ranges to avoid allocations for each recursive call.
	final List<Range> rangePool = new ArrayList<>();
	while (!stack.isEmpty()) {
		final Range range = stack.remove(stack.size() - 1);
		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);
			}
			// offset the snake to convert its coordinates from the Range's area to global
			snake.x += range.oldListStart;
			snake.y += range.newListStart;

			// add new ranges for left and right
			final Range left = rangePool.isEmpty() ? new Range() : rangePool.remove(
					rangePool.size() - 1);
			left.oldListStart = range.oldListStart;
			left.newListStart = range.newListStart;
			if (snake.reverse) {
				left.oldListEnd = snake.x;
				left.newListEnd = snake.y;
			} else {
				if (snake.removal) {
					left.oldListEnd = snake.x - 1;
					left.newListEnd = snake.y;
				} else {
					left.oldListEnd = snake.x;
					left.newListEnd = snake.y - 1;
				}
			}
			stack.add(left);

			// re-use range for right
			//noinspection UnnecessaryLocalVariable
			final Range right = range;
			if (snake.reverse) {
				if (snake.removal) {
					right.oldListStart = snake.x + snake.size + 1;
					right.newListStart = snake.y + snake.size;
				} else {
					right.oldListStart = snake.x + snake.size;
					right.newListStart = snake.y + snake.size + 1;
				}
			} else {
				right.oldListStart = snake.x + snake.size;
				right.newListStart = snake.y + snake.size;
			}
			stack.add(right);
		} else {
			rangePool.add(range);
		}

	}
	// sort snakes
	Collections.sort(snakes, SNAKE_COMPARATOR);
	return new DiffResult(cb, snakes, forward, backward, detectMoves);

}

上面计算不同的代码要理解起来还是有些挑战的,主要用了Myers Diff Algorithm, 而我们日常使用的git diff就用到了该算法。

总结 总体而言,ListAdapter还是比较方便使用的,我现在所有新项目基本都开始采用它,并且结合LiveData使用也会更加方便。

参考 PagedListAdapter ListAdapter