Android-Paging源码分析

1,326 阅读13分钟

Paging架构

  再回头看一眼Paging的架构图 可以看到Paing的核心是PagedList,这个PagedList携带着一个DataSource,也即产生数据的工厂,PagedList中还有分页的配置,例如每页多少个数据,距离底部多少个数据时开始自动加载下一页数据,以及设置加载与更新UI的线程等。
  从触发机制与数据流向角度看,构建PagedList是在页面初始化或者下拉刷新时,使用PagedList.Builder,传入PagedList.Config和DataSource.Factory两个对象。构造出来的PagedList对象调用Adapter.submitList方法传给Adapter,Adapter中持有传入的PagedList对象。submitList还会触发DataSource的loadInitial方法来初始化数据,其实这里初始化数据有两种形式,可以先把数据准备好再调用submitList,在loadInitial方法中直接将数据喂给callback即可,或者还可以在loadInitial方法中触发数据请求,在请求结果回调中调用callback将数据传给Adapter,这些就是触发数据加载的两种逻辑。
  从上图中即可看出数据流向的逻辑,数据从DataSource中生产出来,不管是loadInitial还是loadAfter又或者是loadBefore,都是使用callback调用onResult将数据回传。

构造PagedList

首先来看看PagedList的类结构,其内部有BoundaryCallback、Builder、Callback、Config四个内部类。一个一个来看具体定义
  首先是BoundaryCallback,

@MainThread
    public abstract static class BoundaryCallback<T> {

        public void onZeroItemsLoaded() {}

        public void onItemAtFrontLoaded(@NonNull T itemAtFront) {}

        public void onItemAtEndLoaded(@NonNull T itemAtEnd) {}
    }

把注释都删掉只下定义了3个方法,看这个类的注释,意思大概是如果将本地缓存数据当作网络数据的缓存时,用本地数据构造了一个PagedList并更新到UI上时,这时还是需要通知一下触发一下网络请求的。也就是说我们可以在将数据传给Adapter更新UI的同时异步加载网络数据,并将网络数据储存到本地,而DataSource每次只需要从本地取数据即可,而这个BoundaryCallback就是用来通知触发异步加载网络数据的。
  根据调用堆栈,可发现这个onZeroItemsLoaded是在DataSource的callback.onResult中调用,且callback传回来的数据为空。下面两个方法onItemAtFrontLoaded和onItemAtEndLoaded则分别是在PagedList滑动到可自动加载下一页数据时触发。当DataSource中callback传回数据时,发现当前PagedList数据不为空,而请求传回的数据为空时,这也会被当作是可加载下一页数据。总结一下,BoundaryCallback的三个方法是在自动触发加载下一页数据时回调的,或者是当前PagedList数据不为空,也就是Adapter有内容,而新加载的数据为空时调用。从感性上来说,这个BoundaryCallback应该是一个数据加载监听的辅助接口,用于在加载数据时再异步辅助加载更多数据。
  再看看这个PagedList.Builder类,这纯粹就是个Build模式类,仅仅是为了传入一个参数从而构造出一个PagedList对象。查看其build方法,限制了NotifyExecutor和FetchExecutor不能为null,最后调用了PageList.create方法,将set的参数传入,返回一个PagedList对象。在PagedList.create方法中,有两个分支,分别创建的是ContiguousPagedList和TitledPagedList。判断是dataSource.isContiguous,这个返回值表示两页之间是不是承接式的,也即两页之间的关系是不是当前页和上一页下一页这样的有关系,这样返回的是ContiguousPagedList对象,否则返回TiledPagedList对象,tiled表示是不是平铺的,也即每个Item之间确定关系,是根据item的位置关系来加载更多数据的。
  再来看PagedList.Config类,这个类结构较为简单,只定义了几个字段外加一个Builder

public static class Config {
        @SuppressWarnings("WeakerAccess")
        public static final int MAX_SIZE_UNBOUNDED = Integer.MAX_VALUE;

        public final int pageSize;

        @SuppressWarnings("WeakerAccess")
        public final int prefetchDistance;

        @SuppressWarnings("WeakerAccess")
        public final boolean enablePlaceholders;

        public final int maxSize;

pageSize表示每页的item数量,prefetchDistance表示距离底部或顶部还差多少个item就开始触发loadAfter和loadBefore,enablePlaceHolder表示是否支持展示null placeholders,maxSize表示这个pageList的最大容量。
  PagedList还有一个Callback的内部类

public abstract static class Callback {
        public abstract void onChanged(int position, int count);

        public abstract void onInserted(int position, int count);

        @SuppressWarnings("unused")
        public abstract void onRemoved(int position, int count);
    }

只有三个方法,分别表示PagedList中的数据发生了变化、插入了数据,移除了数据。这个Callback接口只是为了监听PagedList的数据变化,AsyncPagedListDiffer对其的实现是mPagedListCallback对象,在相应的实现方法中直接调用了mUpdateCallback的相应方法,这个mUpdateCallback是AdapterListUpdateCallback类型的。在AdapterListUpdateCallback中的相应方法中,就调用了RecyclerView.Adapter的相应局部刷新api。

public final class AdapterListUpdateCallback implements ListUpdateCallback {
    @NonNull
    private final RecyclerView.Adapter mAdapter;

    public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
        mAdapter = adapter;
    }

    @Override
    public void onInserted(int position, int count) {
        mAdapter.notifyItemRangeInserted(position, count);
    }

    @Override
    public void onRemoved(int position, int count) {
        mAdapter.notifyItemRangeRemoved(position, count);
    }

    @Override
    public void onMoved(int fromPosition, int toPosition) {
        mAdapter.notifyItemMoved(fromPosition, toPosition);
    }

    @Override
    public void onChanged(int position, int count, Object payload) {
        mAdapter.notifyItemRangeChanged(position, count, payload);
    }
}

这样就明白了为什么PagedList的数据变化可以触发Adapter的刷新,从而导致RecyclerView的布局变化了。
  再来看看PagedList的一些成员属性和成员方法。重要的成员属性有两个Executor,分别表示加载数据和刷新UI所在的线程。这里又出来一个新的类PagedStorage,看这名字应该是真正用来储存Page的List,查看其源码,发现其内部有个mPages的ArrayList<List>的变量,而每次对page的变量,都最终会作用到这个mPages的变量来。因此得出结论,PagedList真正用来储存item数据的是这个PagedStorage,虽然PagedList继承自AbstractList,但它对AbstractList的实现都是用的PagedStorage。

submitList执行的操作

  从结构上看了PagedList之后,已经得知数据是如何储存,数据更新是如何调用Adapter去更新UI的,再从数据流角度来看。

public void submitList(@Nullable final PagedList<T> pagedList,
            @Nullable final Runnable commitCallback) {
        if (pagedList != null) {
            if (mPagedList == null && mSnapshot == null) {
                mIsContiguous = pagedList.isContiguous();
            } else {
                if (pagedList.isContiguous() != mIsContiguous) {
                    throw new IllegalArgumentException("AsyncPagedListDiffer cannot handle both"
                            + " contiguous and non-contiguous lists.");
                }
            }
        }

        // incrementing generation means any currently-running diffs are discarded when they finish
        final int runGeneration = ++mMaxScheduledGeneration;

        if (pagedList == mPagedList) {
            // nothing to do (Note - still had to inc generation, since may have ongoing work)
            if (commitCallback != null) {
                commitCallback.run();
            }
            return;
        }

        final PagedList<T> previous = (mSnapshot != null) ? mSnapshot : mPagedList;

        if (pagedList == null) {
            int removedCount = getItemCount();
            if (mPagedList != null) {
                mPagedList.removeWeakCallback(mPagedListCallback);
                mPagedList = null;
            } else if (mSnapshot != null) {
                mSnapshot = null;
            }
            // dispatch update callback after updating mPagedList/mSnapshot
            mUpdateCallback.onRemoved(0, removedCount);
            onCurrentListChanged(previous, null, commitCallback);
            return;
        }

        if (mPagedList == null && mSnapshot == null) {
            // fast simple first insert
            mPagedList = pagedList;
            pagedList.addWeakCallback(null, mPagedListCallback);

            // dispatch update callback after updating mPagedList/mSnapshot
            mUpdateCallback.onInserted(0, pagedList.size());

            onCurrentListChanged(null, pagedList, commitCallback);
            return;
        }

        if (mPagedList != null) {
            // first update scheduled on this list, so capture mPages as a snapshot, removing
            // callbacks so we don't have resolve updates against a moving target
            mPagedList.removeWeakCallback(mPagedListCallback);
            mSnapshot = (PagedList<T>) mPagedList.snapshot();
            mPagedList = null;
        }

        if (mSnapshot == null || mPagedList != null) {
            throw new IllegalStateException("must be in snapshot state to diff");
        }

        final PagedList<T> oldSnapshot = mSnapshot;
        final PagedList<T> newSnapshot = (PagedList<T>) pagedList.snapshot();
        mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
            @Override
            public void run() {
                final DiffUtil.DiffResult result;
                result = PagedStorageDiffHelper.computeDiff(
                        oldSnapshot.mStorage,
                        newSnapshot.mStorage,
                        mConfig.getDiffCallback());

                mMainThreadExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
                        if (mMaxScheduledGeneration == runGeneration) {
                            latchPagedList(pagedList, newSnapshot, result,
                                    oldSnapshot.mLastLoad, commitCallback);
                        }
                    }
                });
            }
        });
    }

首先是PagedListAdapter的submitList方法,这个方法是将生产出来的PagedList直接交由Differ处理。AsyncPagedListDiffer的submitList方法实现是先判断传入的pagedList是否和当前Adapter中的PagedList为同一对象,如果是的话,就直接返回了。再判断传入的PagedList是否为null,如果为null就将Adapter中的当前PagedList也置为null,同时移除对PagedList数据的监听,通知PagedList.Callback的onRemove方法。如果传入的PagedList不为null但Adapter中的当前PagedList为null,则当成是新插入进来的数据 。如果Adapter的当前PagedList也不为null,则从当前PagedList中取出一个Snapshot,也就是一个快照,这里的快照的概念也不复杂,就是如果未detach UI,就新创建一个SnapshotPagedList对象,这个对象的属性全copy自这个PagedList,如果已经detach UI了,则直接返回PagedList自身即可。这是为什么要使用快照而不是使用本身的数据呢?这是因为Paging的Differ过程在异步线程,在diff的过程中,可能数据已经发生了变化。最后从Config中取出后台线程执行Diff操作,Diff的结果再用Main线程执行latchPagedList,将结果更新到Adapter去。这里看一下真正的diff过程,在PagedStorageDiffHelper的computeDiff方法中,computeDiff方法调用了DiffUtil的calculateDiff方法,这里使用了DiffUtil.Callback接口来定义diff。分为三层diff,第一层是areItemsTheSame,表示item对象是否为同一个,第二层是areContentsTheSame,表示item的内容是否相同,第三层是getChangePayload,表示是否有其他的更改。这里的diff过程还使用了Config.getDiffCallback来为DiffUtil的Callback默认实现识别不了的确认最终结果,这个Config.getDiffCallback是我们在创建PagedListAdapter时构造器传入的DiffUtil.ItemCallback对象。

public abstract static class ItemCallback<T> {

        public abstract boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem);

       
        public abstract boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem);

  
        @SuppressWarnings({"WeakerAccess", "unused"})
        @Nullable
        public Object getChangePayload(@NonNull T oldItem, @NonNull T newItem) {
            return null;
        }
    }

这里取出对比的oldItem和newItem也是有说法的,虽说是分别从oldList和newList中取出的,但确实是跳过了PagedList中作为PlaceHolder的item。这里具体的diff逻辑是复用了RecyclerView的diff,这里其实使用差量算法,具体算法原理可见差量算法
  最后在Main线程中调用latchPagedList,在这之前还有一个mMaxScheduledGeneration == runGeneration判断,这个判断是干啥用的呢?可以看到在submitList开始的时候,有个final int runGeneration = ++mMaxScheduledGeneration的操作,这其实是为了防止在diff至更新UI之间的时间内有另一个submitList调用过来,如果有另一个过程,则前一个在进入主线程时,mMaxScheduledGeneration == runGeneration将为false,latchPagedList将不能执行,也即丢弃前一次的diff结果。latchPagedList方法中最主要的操作是调用了PagedStorageDiffHelper的dispatchDiff方法,这个方法将diff结果回调给传入的ListUpdateCallback对象。这个ListUpdateCallback对象也即AdapterListUpdateCallback,也就是前面说过的,相应的方法会调用RecyclerView的局部刷新api。这样submitList的数据从传入pagedList到更新到Adapter进而刷新到RecyclerView的流程也确定了。

自动loadmore原理

  在上一篇文章中介绍如何用Paging实现一个典型的Feed中说到过,自动触发loadmore其实是调用getItem触发的。看下PagedListAdapter的getItem的实现,其实是调用了mDiffer的getItem方法。

public T getItem(int index) {
        if (mPagedList == null) {
            if (mSnapshot == null) {
                throw new IndexOutOfBoundsException(
                        "Item count is zero, getItem() call is invalid");
            } else {
                return mSnapshot.get(index);
            }
        }

        mPagedList.loadAround(index);
        return mPagedList.get(index);
    }

在AsyncPagedListDiffer的getItem方法中,首先判断mPagedList是否为null,如果为null,则从mSnapshot中取,当mSnapshot也为null时,则直接抛个IndexOutOfBoundsException异常。什么时候mPagedList为null,而mSnapshot不为null呢?在AsyncPagedListDiffer的submitList中如果要更新pagedList,则

if (mPagedList != null) {
            // first update scheduled on this list, so capture mPages as a snapshot, removing
            // callbacks so we don't have resolve updates against a moving target
            mPagedList.removeWeakCallback(mPagedListCallback);
            mSnapshot = (PagedList<T>) mPagedList.snapshot();
            mPagedList = null;
        }

这里就是让mPagedList为null而mSnapshot不为null的地方。当mPagedList为null而mSnapshot不为null,也说明正在submit,在刷新列表,这里就不应该触发自动loadmore了,因此就直接return了。否则就调用mPagedList.loadAround方法, PagedList的loadAround中主要是调用了其内部方法loadAroundInternal方法,loadAroundInternal在PagedList中是个抽象方法,在ContiguousPagedList中的实现如下

@MainThread
    @Override
    protected void loadAroundInternal(int index) {
        int prependItems = getPrependItemsRequested(mConfig.prefetchDistance, index,
                mStorage.getLeadingNullCount());
        int appendItems = getAppendItemsRequested(mConfig.prefetchDistance, index,
                mStorage.getLeadingNullCount() + mStorage.getStorageCount());

        mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
        if (mPrependItemsRequested > 0) {
            schedulePrepend();
        }

        mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
        if (mAppendItemsRequested > 0) {
            scheduleAppend();
        }
    }

getPrependItemsRequested是计算当前位置上还剩多少个就要开加载顶部了,而getAppendItemsRequested是计算当前位置上还剩多个少就要加载底部了,具体计算逻辑如下

static int getPrependItemsRequested(int prefetchDistance, int index, int leadingNulls) {
        return prefetchDistance - (index - leadingNulls);
    }

    static int getAppendItemsRequested(
            int prefetchDistance, int index, int itemsBeforeTrailingNulls) {
        return index + prefetchDistance + 1 - itemsBeforeTrailingNulls;
    }

如果要加载顶部就调用schedulePrepend,加载底部就调用scheduleAppend,schedulePrepend和scheduleAppend中分别调用了dataSource的loadBefore和loadAfter。以scheduleAppend为例,scheduleAppend中先计算出要加载的position,然后使用BackgroundThradExecutor执行DataSource的dispatchLoadAfter方法。ItemKeyedDataSource对dispatchLoadAfter的实现是调用其loadAfter方法,这里就到了我们对DataSource实现的loadAfter方法了。而我们使用callback.onResult传回值的callback实现是LoadCallbackImpl类,其onResult方法是调用了LoadCallbackHelper方法的dispatchResultToReceiver。这个dispatchResultToReceiver方法中也是在传入的MainThreadExecutor中执行传入的Receiver的onPageResult方法,这个Receiver是从ContiguousPagedList中传到DataSource中去的,ContiguousPagedList中对这个Receiver的实现如下

PageResult.Receiver<V> mReceiver = new PageResult.Receiver<V>() {
        // Creation thread for initial synchronous load, otherwise main thread
        // Safe to access main thread only state - no other thread has reference during construction
        @AnyThread
        @Override
        public void onPageResult(@PageResult.ResultType int resultType,
                @NonNull PageResult<V> pageResult) {
            if (pageResult.isInvalid()) {
                detach();
                return;
            }

            if (isDetached()) {
                // No op, have detached
                return;
            }

            List<V> page = pageResult.page;
            if (resultType == PageResult.INIT) {
                mStorage.init(pageResult.leadingNulls, page, pageResult.trailingNulls,
                        pageResult.positionOffset, ContiguousPagedList.this);
                if (mLastLoad == LAST_LOAD_UNSPECIFIED) {
                    // Because the ContiguousPagedList wasn't initialized with a last load position,
                    // initialize it to the middle of the initial load
                    mLastLoad =
                            pageResult.leadingNulls + pageResult.positionOffset + page.size() / 2;
                }
            } else {
                // if we end up trimming, we trim from side that's furthest from most recent access
                boolean trimFromFront = mLastLoad > mStorage.getMiddleOfLoadedRange();

                // is the new page big enough to warrant pre-trimming (i.e. dropping) it?
                boolean skipNewPage = mShouldTrim
                        && mStorage.shouldPreTrimNewPage(
                                mConfig.maxSize, mRequiredRemainder, page.size());

                if (resultType == PageResult.APPEND) {
                    if (skipNewPage && !trimFromFront) {
                        // don't append this data, drop it
                        mAppendItemsRequested = 0;
                        mAppendWorkerState = READY_TO_FETCH;
                    } else {
                        mStorage.appendPage(page, ContiguousPagedList.this);
                    }
                } else if (resultType == PageResult.PREPEND) {
                    if (skipNewPage && trimFromFront) {
                        // don't append this data, drop it
                        mPrependItemsRequested = 0;
                        mPrependWorkerState = READY_TO_FETCH;
                    } else {
                        mStorage.prependPage(page, ContiguousPagedList.this);
                    }
                } else {
                    throw new IllegalArgumentException("unexpected resultType " + resultType);
                }

                if (mShouldTrim) {
                    if (trimFromFront) {
                        if (mPrependWorkerState != FETCHING) {
                            if (mStorage.trimFromFront(
                                    mReplacePagesWithNulls,
                                    mConfig.maxSize,
                                    mRequiredRemainder,
                                    ContiguousPagedList.this)) {
                                // trimmed from front, ensure we can fetch in that dir
                                mPrependWorkerState = READY_TO_FETCH;
                            }
                        }
                    } else {
                        if (mAppendWorkerState != FETCHING) {
                            if (mStorage.trimFromEnd(
                                    mReplacePagesWithNulls,
                                    mConfig.maxSize,
                                    mRequiredRemainder,
                                    ContiguousPagedList.this)) {
                                mAppendWorkerState = READY_TO_FETCH;
                            }
                        }
                    }
                }
            }

            if (mBoundaryCallback != null) {
                boolean deferEmpty = mStorage.size() == 0;
                boolean deferBegin = !deferEmpty
                        && resultType == PageResult.PREPEND
                        && pageResult.page.size() == 0;
                boolean deferEnd = !deferEmpty
                        && resultType == PageResult.APPEND
                        && pageResult.page.size() == 0;
                deferBoundaryCallbacks(deferEmpty, deferBegin, deferEnd);
            }
        }
    };

代码虽然有点多,但逻辑其实挺简单的,有个if-else分别,分别表示是否是INIT,这个INIT就是loadInitial方法的onResult,如果是INIT就调用PagedStorage的init方法进行初始化,否则就要在else分支中判断是添加到顶部还是底部,判断依据仍然是resultType,也就是说这个resultType有INIT、PREPEND和APPEND三种取值。最后都会将callback.onResult传回来的Item的list会被添加到PagedStorage中去,分别是PagedStorage的prependPage方法和appendPage方法,在添加的过程中,PagedStorage中回调了传入的callback,回调的实现类就是ContiguousPagedList本身,这个回调的实现就是调用了ContiguousPagedList的notify相关方法。而前面也讲过了,这些notify相关方法中是调用了PagedList.Callback的相关回调方法,而AsyncPagedListDiffer中对PagedList.Callback的实现是调用了ListUpdateCallback的相关回调方法。ListUpdateCallback的实现类是AdapterListUpdateCallback,里面的相关回调方法中直接调用了RecyclerView.Adapter的局部刷新api。

总结

  从整个结构和数据流程上来讲,Paging框架并没有实现什么高科技的操作,都是复用的RecyclerView.Adapter的局部刷新api,理论来说如果要求对性能更高,直接用RecyclerView.Adapter的局部刷新api,性能可能会更好一些。个人感觉Paging是为实现一个典型的Feed流提供了一个通用框架,让开发者不用基于滑动手势监听来触发loadmore了,也不用自己维护一个Item的list,处理各种局部刷新的场景,因为Paging只提供了一个刷新的api——submitList方法。
  但Paging唯一可被吐槽的劣势也很明显,那就是它和RecyclerView.Adapter一样,需要通过继承才能使用Paging的能力。如果已有业务方想要接入Paging,但不想替换掉已有的Adapter,那可能就很难接入Paging了,例如我在公司的直播中台和小说业务方,他们的Adapter是个RecyclerView.Adapter,而且还经过了他们的深度定制,要他们因为接入我们的app而改变Adapter的继承结构,很显示是不现实的,他们这种中台类业务方,是同一套sdk接入多个app,因此只能让我们适配他们。
  对于Adapter无法打断继承结构,我的做法是参考HeaderAndFooterRecyclerViewAdapter的做法,用 RecyclerView.AdapterDataObserver监听原生的RecyclerView.Adapter的数据变化,也就是原生RecyclerView.Adapter调用了notify相关方法时,会触发AdapterDataObserver的相关方法回调,在这些刷新方法中调用PagedListAdapter的submitList方法,这样就能以Wapper的方式用PagedListAdapter将RecyclerView.Adapter给包一层了。RecyclerView真正使用的是PagedListAdapter,而中台业务方却还以为使用自己的Adapter,以此来达到对业务方使用透明的目的,只要不乱用RecyclerView.getAdapter instanceOf CustomAdapter这种操作。