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这种操作。