MENU

RecyclerView使用优化的ListAdapter

February 22, 2021 • Read: 524 • Android

前言

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