Paging2之DataSource 个人小结
来源:5-8 paging框架工作原理2【难点】

慕慕6500093
2022-11-17
3) Paging 2
Paging是JetPack中的一个数据分页加载组件,核心成员有 DataSource, PagedList, PagedListAdapter。
一、DataSource
Base class for loading pages of snapshot data into a PagedList.
DataSource is queried to load pages of content into a PagedList. A PagedList can grow as it loads more data, but the data loaded cannot be updated. If the underlying data set is modified, a new PagedList / DataSource pair must be created to represent the new data.
也就是说DataSuorce是一个提供数据来源,并将拿到的数据以内容页加载到PagedList(其实就是个list)中。PagedList可以随着加载更多数据而增长,但加载的数据无法更新。如果修改了基础数据集,则必须创建一个新的PagedList/DataSource对来表示新数据。
PagedList queries data from its DataSource in response to loading hints. PagedListAdapter calls PagedList.loadAround(int) to load content as the user scrolls in a RecyclerView.
To control how and when a PagedList queries data from its DataSource, see PagedList.Config. The Config object defines things like load sizes and prefetch distance.
PagedList从datasouce拿到数据,然后PagedListAdapter调用PagedList.loadAround(int)在用户在RecyclerView中滚动时加载内容。
而PagedList.Config对象定义了每次加载的大小和预加载数量等参数。
先看下 PagedList.Config 参数配置:
// 每次分页加载的数量
.setPageSize(pageSize)
// 初始化时加载的数量(该值通常大于pageSize)
.setInitialLoadSizeHint(initialLoadSizeHint)
// 预加载的数量(默认和pageSize相等)
//.setPrefetchDistance(prefetchDistance)
// 数据源最大可加载的数量
//.setMaxSize(maxSize)
// 未加载出来的条目是否用占位符替代(默认true,启用的话必须设置maxSize)
//.setEnablePlaceholders(false)
.build()
To implement, extend one of the subclasses: PageKeyedDataSource, ItemKeyedDataSource, or PositionalDataSource.
由于datasource是一个抽象类,它的实现类有PageKeyedDataSource,ItemKeyedDataSource, or PositionalDataSource。
Use PageKeyedDataSource if pages you load embed keys for loading adjacent pages. For example a network response that returns some items, and a next/previous page links.
如果页面加载了用于加载相邻页面的key,请使用PageKeyedDataSource。例如,返回一些项目的网络响应,以及下一页/上一页信息。(也就是根页的信息key来请求数据的场景)
Use ItemKeyedDataSource if you need to use data from item N-1 to load item N. For example, if requesting the backend for the next comments in the list requires the ID or timestamp of the most recent loaded comment, or if querying the next users from a name-sorted database query requires the name and unique ID of the previous.
如果需要使用项N-1中的数据来加载项N,请使用ItemKeyedDataSource。key是依赖特定item中的信息。
Use PositionalDataSource if you can load pages of a requested size at arbitrary positions, and provide a fixed item count. PositionalDataSource supports querying pages at arbitrary positions, so can provide data to PagedLists in arbitrary order. Note that PositionalDataSource is required to respect page size for efficient tiling. If you want to override page size (e.g. when network page size constraints are only known at runtime), use one of the other DataSource classes.
如果可以在任意位置加载请求大小的页面,并提供固定的项目计数,请使用PositionalDataSource。也就是总数固定,通过特定的position位置加载数据,这里Key是Integer类型的position位置信息。
以上就是paging自带的几个常用的数据DataSource<Key,Value>. key是指加载数据的条件信息,value是指对应的数据实体类。
To page in data from a source that does provide updates, you can create a DataSource.Factory, where each DataSource created is invalidated when an update to the data set occurs that makes the current snapshot invalid. For example, when paging a query from the Database, and the table being queried inserts or removes items. You can also use a DataSource.Factory to provide multiple versions of network-paged lists. If reloading all content (e.g. in response to an action like swipe-to-refresh) is required to get a new version of data, you can connect an explicit refresh signal to call invalidate() on the current DataSource.
也就是说,可以通过DataSource.Factory来提供数据源,如果需要重新加载(例如:页面刷新),可以调用DataSource.invalidate().
val factory = object : DataSource.Factory<Any, T>() {
override fun create(): DataSource<Any, T> {
// createDataSource()
// 数据来源可以是DB缓存,网络...
// 创建的实例可以是PageKeyedDataSource/ItemKeyedDataSource/PositionalDataSource
}
}
以ItemKeyedDataSource为例,跟PageKeyedDataSource一样,也是继承ContiguousDataSource(连续的数据源)的子类:
奇怪,上传的截图不显示。。。
由于ItemKeyedDataSource也是抽象类,需要实现上面几个方法:
(1) getKey(item): 上面讲过,key是依赖特定item中的信息,一般为id.
(2) loadInitial(params,callback): 加载初始化数据。追溯源码 LivePagedListBuilder.build() —> create()—> ComputableLiveData.compute()—> PagedList.build()—> PagedList.create —> new ContiguousPagedList() —> dispatchLoadInitial —> loadInitial
发现原来是LivePagedListBuilder执行build()方法时触发的。// 注意:compute()时会调用getKey().
(3) loadAfter(params,callback):向后加载分页数据。那么它是什么时候触发的呢(这个问题之前想了很久)?后来通过上面Google对DataSource的描述“PagedList queries data from its DataSource in response to loading hints. PagedListAdapter calls PagedList.loadAround(int) to load content as the user scrolls in a RecyclerView.”)才知,滑动的时候会调用PagedList.loadAround(int),而往上追溯发现是AsyncPagedListDiffer的getItem(int index)触发的,Google对这个AsyncPagedListDiffer类的描述是“Helper object for mapping a PagedList into a RecyclerView.Adapter.”,也就是用于将PagedList映射到RecyclerView.Adapter的Helper对象。这就大概明白了。
源码追溯:滑动会触发getItem(int)—> PageList.loadAround—> ContiguousPagedList.loadAroundInternal—> scheduleAppend—> (后台线程中执行)mDataSource.dispatchLoadAfter—> loadAfter(params,callback)
我们就可以在loadAfter(params,callback)做获取数据的操作(如拿本地缓存、网络数据等),那么数据是怎么传给AsyncPagedListDiffer这个对象的呢?这里面有个callback(只有一个回调方法onResult(data)),Google描述是“Called to pass loaded data from a DataSource.”,也就是调用以传递来自DataSource的加载的数据,即onResult(data)是用来传递ItemKeyedDataSource拿到的数据用的,
通过源码:
@Override
final void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem,
int pageSize, @NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> receiver) {
loadAfter(new LoadParams<>(getKey(currentEndItem), pageSize),
new LoadCallbackImpl<>(this, PageResult.APPEND, mainThreadExecutor, receiver));
}
可知,实际上callback传的是LoadCallbackImpl,type=PageResult.APPEND
static class LoadCallbackImpl<Value> extends LoadCallback<Value> {
final LoadCallbackHelper<Value> mCallbackHelper;
LoadCallbackImpl(@NonNull ItemKeyedDataSource dataSource, @PageResult.ResultType int type,
@Nullable Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> receiver) {
mCallbackHelper = new LoadCallbackHelper<>(
dataSource, type, mainThreadExecutor, receiver);
}
@Override
public void onResult(@NonNull List<Value> data) {
if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
}
}
}
调用的是 mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0)),也就是ItemKeyedDataSource拿到的data数据传给了它,
void dispatchResultToReceiver(final @NonNull PageResult<T> result) {
Executor executor;
synchronized (mSignalLock) {
// 这个mHasSignalled只有这一个地方赋值
// 也就说明这个dispatchResultToReceiver方法一旦执行过一次,
// 就不允许再执行了,否则直接抛异常
if (mHasSignalled) {
throw new IllegalStateException(
"callback.onResult already called, cannot call again.");
}
mHasSignalled = true;
executor = mPostExecutor;
}
if (executor != null) {// 如果有设置mPostExecutor则异步执行
executor.execute(new Runnable() {
@Override
public void run() {
// mResultType=PageResult.APPEND, result=data
mReceiver.onPageResult(mResultType, result);
}
});
} else {
mReceiver.onPageResult(mResultType, result);
}
}
接着看mReceiver.onPageResult(mResultType, result);
PageResult.Receiver<V> mReceiver = new PageResult.Receiver<V>() {
//。。。
@AnyThread
@Override
public void onPageResult(@PageResult.ResultType int resultType,
@NonNull PageResult<V> pageResult) {
// 数据无效(即空数据集合)
if (pageResult.isInvalid()) {
// Detach the PagedList from its DataSource, and attempt to load no more data.
// 将PagedList与其DataSource分离,并尝试不再加载更多数据。
detach();
return;
}
// 如果PagedList与其DataSource分离(即不再有关联),则不管它
if (isDetached()) {
// No op, have detached
return;
}
List<V> page = pageResult.page;
// 如果是PageResult.INIT,也就是loadInitial回传过来的,
if (resultType == PageResult.INIT) {
// 那么mStorage(是个PagedStorage,专门存储分页数据的一个东东)将初始化分页数据。
// 追溯源码:PageStorage.init—> callback.onInitialized(size())—> onInitialized—> onInitialized—> mUpdateCallback.onInserted(position, count)—> mAdapter.notifyItemRangeInserted(position, count)
// 我的乖乖,最后还是通过把PagedList映射到RecyclerView.Adapter的Helper对象AsyncPagedListDiffer处理的,然后更新UI。
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 {
// 追溯源码:PageStorage.appendPage—> callback.onPageAppended—> notifyInserted(endPosition + changedCount, addedCount)-->...--> mAdapter.notifyItemRangeInserted(position, count) 然后更新UI
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);
}
}
// 如果LivePagedListBuilder设置了boundaryCallback(边界回调)
if (mBoundaryCallback != null) {
//...
deferBoundaryCallbacks(deferEmpty, deferBegin, deferEnd);
}
}
};
分发边界回调 dispatchBoundaryCallbacks:
void dispatchBoundaryCallbacks(boolean begin, boolean end) {
// safe to deref mBoundaryCallback here, since we only defer if mBoundaryCallback present
if (begin) {
// 第一条数据被加载
mBoundaryCallback.onItemAtFrontLoaded(mStorage.getFirstLoadedItem());
}
if (end) {
// 最后一条数据被加载
mBoundaryCallback.onItemAtEndLoaded(mStorage.getLastLoadedItem());
}
}
最后再看下前面说的DataSource.invalidate()干了啥?
源码追溯:onInvalidated —> invalidate()—> mInvalidationRunnable
final Runnable mInvalidationRunnable = new Runnable() {
@MainThread//(发生在主线程)
@Override
public void run() {
boolean isActive = mLiveData.hasActiveObservers();
if (mInvalid.compareAndSet(false, true)) {//将livedata的mInvalid 有效--> 无效
if (isActive) {// 有活跃的观察者,执行mRefreshRunnable
mExecutor.execute(mRefreshRunnable);
}
}
}
};
// 子线程中执行
final Runnable mRefreshRunnable = new Runnable() {
@WorkerThread
@Override
public void run() {
boolean computed;
do {
computed = false;
// compute can happen only in 1 thread but no reason to lock others.
if (mComputing.compareAndSet(false, true)) {
// as long as it is invalid, keep computing.
try {
T value = null;
// livedata的mInvalid 无效-> 有效
// compareAndSet原子操作,没成功一直执行
while (mInvalid.compareAndSet(true, false)) {
computed = true;
// 这里compute()方法会重新创建一对新的PagedList/datasource
// 又会执行PagedList.build()方法触发loadInitial 重新刷新页面骚操作
value = compute();
}
if (computed) {
// 触发liveData观察者的异步onChanged()回调
mLiveData.postValue(value);
}
} finally {
// release compute lock
mComputing.set(false);
}
}
//...
} while (computed && mInvalid.get());
}
};
DataSource 小结:
- DataSource就是个提供数据的窗口,一般有三种,常用的就ItemKeyedDataSource。
- AsyncPagedListDiffer是一个将PagedList映射到RecyclerView.Adapter的Helper对象。
- callback.onResult(data)是用来回传DataSource拿到的数据。具体流程:数据有经过包装给了dispatchResultToReceiver,接着触发了mReceiver.onPageResult(mResultType, result)….最终触发了mAdapter.notifyItemRangeInserted(position, count)刷新界面。
- 两个触发loadInitial()的地方:一个是LivePagedListBuilder.create(),第二个是DataSource.invalidate(),因为这两个都会触发mRefreshRunnable执行,进而调用compute()实例化PagedList(PagedList.build()–> PagedList.create)/DataSource,这俩货是成对出现且PagedList.Builder持有DataSource对象。
有个疑问,希望老师回复一下:
就是分析执行mRefreshRunnable这的时候:
while (mInvalid.compareAndSet(true, false)) {
computed = true;
// 这里compute()方法会重新创建一对新的PagedList/datasource
// 又会执行PagedList.build()方法触发loadInitial 重新刷新页面骚操作
value = compute();
}
if (computed) {
// 触发liveData观察者的异步onChanged()回调
mLiveData.postValue(value);
}
compute()会触发一次页面刷新,而mLiveData.postValue异步发射数据后,livedata.observe(viewLifecycleOwner){ pagedList -> submitList(pagedList) }这里也会跟着触发页面刷新?是会有2次刷新吗?还是说submitList内部会对pagedList做差异处理,跟原来相同就不更新adapter刷新页面了?
写回答
1回答
-
LovelyChubby
2022-11-23
掌声送给你00
相似问题