??距離上一篇Jetpack源碼分析的文章已經(jīng)兩個(gè)月铛绰,時(shí)間間隔確實(shí)有點(diǎn)長舞吭。最近绰上,感覺自己的學(xué)習(xí)積極性不那么的高旨怠,看Paging的源碼也是斷斷續(xù)續(xù)的。時(shí)至今日蜈块,才算是完成對Paging的源碼學(xué)習(xí)鉴腻。今天我們就來學(xué)習(xí)Paging的實(shí)現(xiàn)原理。
??本文參考資料:
??注意百揭,本文Paging相關(guān)源碼均來自于2.1.2版本爽哎。
1. 概述
??在日常開發(fā)中,我們經(jīng)常能接觸得到一個(gè)場景--需要加載列表數(shù)據(jù)器一,通常來說课锌,列表數(shù)據(jù)的顯示可以用RecyclerView,但是列表數(shù)據(jù)的加載并沒有現(xiàn)成的庫或者工具類供我們使用祈秕。從另一個(gè)方面來說产镐,對于大量的列表數(shù)據(jù),我們不可能一次將它一次性從后臺(tái)獲取過來踢步,所以分頁加載是必要的事癣亚。
??由于Google爸爸沒有給我們提供現(xiàn)成的輪子,所以在此之前获印,我們需要分頁加載述雾,都是自己簡單的實(shí)現(xiàn)。通常實(shí)現(xiàn)方案是:通過RecyclerView對OnScrollerListener
的onScrolled方法的回調(diào)兼丰,我們可以在這個(gè)方法里面監(jiān)聽并且計(jì)算位置玻孟,當(dāng)符合加載時(shí)機(jī)時(shí),就可以加載下一頁的數(shù)據(jù)鳍征。
??上面的實(shí)現(xiàn)方案是無可厚非的黍翎,并且還比較簡單,實(shí)現(xiàn)起來也比較容易艳丛。但是有啥問題呢匣掸?我們從下面幾個(gè)方面來看看:
- 耦合度比較高。OnScrollerListener在計(jì)算位置的時(shí)候氮双,通常來說會(huì)依賴RecyclerView的LayoutManager碰酝,不同LayoutManager有不同計(jì)算方式,如果后面RecyclerView有很多不同的LayoutManager戴差,OnScrollerListener里面就會(huì)變的非常復(fù)雜送爸。
- 擴(kuò)展性比較差。這個(gè)可以從兩個(gè)方面來介紹:首先,在不同的業(yè)務(wù)場景中袭厂,加載下一頁的方式可能不一樣墨吓,可能是通過Position獲取下一頁,也有可能是通過key獲取下一次纹磺,針對于此類情形肛真,OnScrollerListener必須單獨(dú)處理;其次爽航,通常來說蚓让,RecyclerView不僅僅有加載下一頁數(shù)據(jù)的場景,也有可能加載上一頁的場景的讥珍,針對于此類情形历极,OnScrollerListener也需要單獨(dú)處理。
??其實(shí)衷佃,理想的情況是每種業(yè)務(wù)場景互相互相獨(dú)立趟卸,而不是糅合在一個(gè)類里面。當(dāng)然氏义,這些問題其實(shí)可有可無锄列,因?yàn)榻?jīng)過簡單的拆分和整理,還是可以完全避免惯悠。而我們今天介紹的Paging
邻邮,是Google爸爸為了解決分頁加載的問題而推出的一個(gè)庫。出于偷懶的原則克婶,既然Google爸爸已經(jīng)為我們實(shí)現(xiàn)了筒严,我們?yōu)樯哆€要自己搞呢,對吧情萤?
??針對于Paging鸭蛙,我也不過多的介紹它是啥,它是怎么使用筋岛,相信大家非常的熟悉娶视。我們就直接進(jìn)入本文的主題--從源碼角度來學(xué)習(xí)一下Paging的實(shí)現(xiàn)原理。
2. 基本架構(gòu)
??Paging雖然是Jetpack成員中的一份子睁宰,但是卻跟其他成員(Lifecycle肪获、ViewModel等)不一樣。其他的成員可能就是幾個(gè)類就能搞定實(shí)現(xiàn)勋陪,但是Paging卻不一樣贪磺,里面涉及的類特別的多,所以在正式分析它的源碼之前诅愚,我們先來看一下它的架構(gòu)實(shí)現(xiàn)。同時(shí),我們從這里可以看出來违孝,Google爸爸對Paging的期望很高刹前,否則為啥會(huì)不遺余力的設(shè)計(jì)和實(shí)現(xiàn)它。
??從實(shí)現(xiàn)上來看雌桑,Paging
主要分為3個(gè)部分:PagedListAdapter
喇喉、PagedList
和DataSource
。這其中校坑,PagedListAdapter
和DataSource
比較熟悉拣技,因?yàn)槲覀冊谑褂眠^程中必須自定義它倆,相對而言耍目,我們對PagedList
要陌生一些膏斤。不管怎么樣,我們都先來了解一下它們邪驮。
(1). PagedListAdapter
??從本質(zhì)上來說莫辨,PagedListAdapter
其實(shí)就是RecyclerView中 的Adapter的實(shí)現(xiàn)類,本身承載的作用就是Adapter本身的作用毅访。不過沮榜,相比于其他的Adapter,PagedListAdapter
的內(nèi)部卻也有些不同喻粹。
??PagedListAdapter
內(nèi)部有一個(gè)AsyncPagedListDiffer
類蟆融,這個(gè)類接管了Adapter對數(shù)據(jù)源的所有操作,其中包括:
- submitList:該方法的作用就是給Adapter設(shè)置一個(gè)新的數(shù)據(jù)源守呜,由于Adapter可能存在的舊數(shù)據(jù)源振愿,所以需要使用DiffUtil來進(jìn)行差量計(jì)算。
AsyncPagedListDiffer
將這個(gè)方法的具體操作接管了過去弛饭,其實(shí)內(nèi)部就是進(jìn)行差量計(jì)算冕末。我們通過這個(gè)方法的參數(shù),還可以注意到一個(gè)小細(xì)節(jié)侣颂,就是該方法的參數(shù)是一個(gè)PagedList
档桃。進(jìn)而可以知道,AsyncPagedListDiffer
內(nèi)部的維護(hù)PagedList對象憔晒。- getItem:該方法的作用是從數(shù)據(jù)源中獲取對應(yīng)位置的Data數(shù)據(jù)藻肄。
AsyncPagedListDiffer
將其也接管過去了,其內(nèi)部實(shí)現(xiàn)其實(shí)就是從PagedList
里面獲取拒担,PagedList
的本質(zhì)就是一個(gè)List嘹屯。需要特別注意的是,該方法的數(shù)據(jù)可能會(huì)為空从撼,所以一定要做防空的保護(hù)州弟,具體為啥會(huì)為空呢?待會(huì)我們分析在PagedList會(huì)重點(diǎn)介紹。- getItemCount:該方法的作用是返回?cái)?shù)據(jù)源的總個(gè)數(shù)婆翔。同
getItem
方法拯杠,該方法也被AsyncPagedListDiffer
接管過去了。
??總的來說啃奴,AsyncPagedListDiffer
接管了Adapter對數(shù)據(jù)源的操作潭陪,同時(shí)在這個(gè)過程中還承擔(dān)了一個(gè)角色:作為PagedList操作Adapter的中間橋梁。
??可能有人會(huì)問最蕾,什么是PagedList操作Adapter依溯?我們知道,當(dāng)數(shù)據(jù)源發(fā)生了改變瘟则,比如說進(jìn)行了add黎炉、remove或者update的操作,要想操作生效壹粟,必須調(diào)用對應(yīng)的notifyXXX方法拜隧。AsyncPagedListDiffer
在初始化PagedList時(shí),會(huì)向其中注冊一個(gè)回調(diào)接口趁仙,用來監(jiān)聽這一部分的操作洪添,當(dāng)回調(diào)產(chǎn)生,會(huì)調(diào)用Adapter對應(yīng)的方法雀费。這個(gè)待會(huì)我們在分析源碼干奢,可以簡單的從源碼角度看一下。
(2). PagedList
??PagedList
相較于PagedListAdapter
來說盏袄,要稍微復(fù)雜忿峻。我們主要從兩個(gè)方面看一下PagedList:
PagedList本身是基類,提供很多通用的方法辕羽,比如說
size
方法逛尚、getLastKey
方法等。這些方法每個(gè)子類的實(shí)現(xiàn)都差不多刁愿,但是isContiguous
方法就不一樣绰寞,它可以將PagedList分為兩個(gè)部分:連續(xù)的還是非連續(xù)的。那么我們怎么來理解這連續(xù)的概念呢铣口?我們知道數(shù)據(jù)都是通過分頁加載的方式滤钱,連續(xù)的數(shù)據(jù),我們理解為下一頁的數(shù)據(jù)跟上一頁的數(shù)據(jù)有一定的關(guān)系脑题,比如說下一頁的數(shù)據(jù)是通過上一頁某一個(gè)key獲取的得來件缸;非連續(xù)的數(shù)據(jù),我們可以連接為下一頁的數(shù)據(jù)跟上一頁的數(shù)據(jù)沒有關(guān)系叔遂,比如說PositionalDataSource
是完全通過position來獲取數(shù)據(jù)他炊,當(dāng)然從一定意義來說争剿,連續(xù)性的數(shù)據(jù)和非連續(xù)性的數(shù)據(jù)沒有本質(zhì)的區(qū)別,這個(gè)我們在后面可以看到佑稠。
??通過isContiguous
方法劃分秒梅,我們大致可以將PageList分為兩類:
??從上面的uml類圖中旗芬,我們知道連續(xù)的PagedList對應(yīng)的實(shí)現(xiàn)類是ContiguousPagedList
,非連續(xù)的PagedList
對應(yīng)的實(shí)現(xiàn)類是TiledPagedList
舌胶。從uml類圖,我們還可以得到一個(gè)信息就是疮丛,就是這兩個(gè)個(gè)部分的PagedList關(guān)心的重點(diǎn)是不一樣的:
- ContiguousPagedList關(guān)心的是
onPagePrepended
和onPageAppended
幔嫂,也就是說,連續(xù)的PagedList關(guān)心的是上一頁數(shù)據(jù)和下一頁數(shù)據(jù)的加載誊薄。同時(shí)我們從源碼可以簡單的看到履恩,類似于onPageInserted
這類TiledPagedList
比較關(guān)心的方法,在ContiguousPagedList
的內(nèi)部是不支持的呢蔫。- TiledPagedList關(guān)心的是
onPageInserted
方法切心,也就是說,非連續(xù)的PagedList關(guān)心的是數(shù)據(jù)的插入片吊,這里我們將其理解為下一頁數(shù)據(jù)的加載绽昏。同理,ContiguousPagedList
關(guān)心的方法在TiledPagedList
的內(nèi)部也是不支持的俏脊。
??PagedList還有一個(gè)簡單的實(shí)現(xiàn)類--SnapshotPagedList
全谤,該類的實(shí)現(xiàn)比較簡單,且用途單一爷贫,本文就不討論了(不知道Google爸爸實(shí)現(xiàn)這個(gè)類干嘛用的认然,很雞肋)。
??PagedList從本質(zhì)來說漫萄,就是一個(gè)List接口的實(shí)現(xiàn)類卷员,跟ArrayList差不多推励,其實(shí)就是集合瘪板,所以Adapter通過它來獲取對應(yīng)的Data,也是不無道理的悯蝉。與ArrayList不同的是窑睁,PagedList
還負(fù)責(zé)加載數(shù)據(jù)的功能(實(shí)則不是PagedList來加載挺峡,而是通過PagedList通知dataSource來加載數(shù)據(jù)。)担钮。
(3). DatDataSource
??要說這三兄弟中最復(fù)雜的部分非DataSource莫屬橱赠,DatDataSource
復(fù)雜點(diǎn)主要是體現(xiàn)如下兩個(gè)方面:
- DataSource的實(shí)現(xiàn)類比較多。跟PagedList比較類似箫津,DataSource也可以非分為連續(xù)的和非連續(xù)的狭姨;但是跟PagedList不一樣宰啦,每個(gè)部分的實(shí)現(xiàn)類均還有實(shí)現(xiàn)類(主要分頁加載的場景比較多。)饼拍。
- DataSource承擔(dān)的功能比較復(fù)雜赡模。顧名思義,我們從
DataSource
的名字师抄,就知道它的作用是產(chǎn)生和維護(hù)數(shù)據(jù)漓柑。
??我們先來簡單的看一下DatDataSource的uml類圖:
??跟PagedList類似,我們可以從上面的uml類圖發(fā)現(xiàn)叨吮,連續(xù)的和非連續(xù)的DataSource的重點(diǎn)是不一樣的辆布,這里就不反復(fù)介紹了。
(4).三兄弟的關(guān)系
??上面分別介紹了一下三兄弟的各自作用茶鉴,在這里锋玲,我們簡單的看一下這三兄弟的關(guān)系,即它們?nèi)值苁窃趺绰?lián)系來的涵叮。
- PagedListAdapter:直接面對RecyclerView惭蹂,只是要從PagedList里面獲取對應(yīng)的Data。同時(shí)割粮,加載下一頁數(shù)據(jù)的時(shí)機(jī)也是由它觸發(fā)的盾碗,Adapter通過
getItem
方法從PagedList中獲取數(shù)據(jù)的同時(shí),還通過調(diào)用PagedList的loadAround
方法觸發(fā)加載下一頁數(shù)據(jù)的時(shí)機(jī)穆刻。- PagedList:首先是給
PagedListAdapter
提供對應(yīng)的接口置尔,讓其能夠獲取數(shù)據(jù)以及加載下一頁的數(shù)據(jù);其次就是氢伟,直接持有DataSource的引用榜轿,可以直接對其進(jìn)行對應(yīng)的操作,比如說朵锣,加載數(shù)據(jù)等谬盐。- DataSource:三兄弟中最底層和最累的一個(gè),主要是對
PagedList
提供接口诚些,讓其能夠進(jìn)行對應(yīng)的操作飞傀。
??到這里,我們對Paging庫里面基本組成部分有了一個(gè)大概的了解诬烹,接下來我們將從源碼角度來分析一下Paging的主要實(shí)現(xiàn)原理砸烦,本文主要從如下幾個(gè)方面來分析Paging:
- paging如何進(jìn)行初始化第一頁數(shù)據(jù)(類似于刷新)。
- paging如何加載下一頁的數(shù)據(jù)绞吁。
- 從源碼角度來分析 PagedList的Config配置幢痘。
3.數(shù)據(jù)的加載
??我們都知道paging是用來進(jìn)行分頁加載的,所謂分頁加載家破,重點(diǎn)當(dāng)然在加載颜说,進(jìn)一步的細(xì)化购岗,我們需要了解的是:paging是怎么初始化數(shù)據(jù),以及怎么加載下一頁數(shù)據(jù)的门粪。這里喊积,我們分開來看這個(gè)方面,至于paging的基本使用玄妈,本文就不介紹了乾吻,不熟悉的同學(xué)可以參考 Android Jetpack- paging的基本使用這篇文章。
(1).加載第一頁數(shù)據(jù)措近。
??通常來說溶弟,加載第一頁數(shù)據(jù)的方式不僅是第一次加載數(shù)據(jù)女淑,還有一種方式就是通過刷新加載數(shù)據(jù)瞭郑,此種方式會(huì)使之前的PagedList完全,進(jìn)而重新創(chuàng)建一個(gè)新的PagedList對象來存儲(chǔ)數(shù)據(jù)鸭你。
??雖然說加載的方式有兩種屈张,但是從源碼角度來看,其實(shí)都是一樣的袱巨,接下來我們看一下對應(yīng)的源碼阁谆。
??通常來說,我們使用Paging愉老,都是在ViewModel里面創(chuàng)建一個(gè)LiveData<PagedList>對象场绿,我們就從這個(gè)點(diǎn)開始分析源碼。我們可以通過如下的方式創(chuàng)建LiveData<PagedList>對象:
val mPageListLiveData = LivePagedListBuilder(mFactory, PagedList.Config.Builder().apply {
setPageSize(20)
setEnablePlaceholders(true)
}.build()).build()
??LiveData<PagedList>對象是通過LivePagedListBuilder
的build方法創(chuàng)建的嫉入,這其中LivePagedListBuilder
的構(gòu)造方法焰盗,第一個(gè)參數(shù)是DataSource.Factory
,該工廠類的作用用來創(chuàng)建DataSource對象咒林,所以我們使用Paging
的步驟中熬拒,一個(gè)必不可少的步驟就是創(chuàng)建對應(yīng)的DataSource的工廠類;第二參數(shù)就是創(chuàng)建PagedList.Config對象垫竞,主要的作用是設(shè)置分頁加載基本參數(shù)澎粟,比如說每頁加載大的大小以及預(yù)取下一頁的距離等。
??假設(shè)我們正確的配置了分頁加載的基本參數(shù)(我們這里強(qiáng)調(diào)了正確的配置欢瞪,顧名思義也有錯(cuò)誤的配置活烙,這個(gè)我們在后面分析Config會(huì)重點(diǎn)介紹。)遣鼓,最后就是調(diào)用LivePagedListBuilder
的build方法創(chuàng)建LiveData<PagedList>對象啸盏。我們來看看build方法的實(shí)現(xiàn):
@NonNull
@SuppressLint("RestrictedApi")
public LiveData<PagedList<Value>> build() {
return create(mInitialLoadKey, mConfig, mBoundaryCallback, mDataSourceFactory,
ArchTaskExecutor.getMainThreadExecutor(), mFetchExecutor);
}
??build
方法本身沒有做什么事,直接調(diào)用了create方法譬正,我們看一下create方法實(shí)現(xiàn):
@AnyThread
@NonNull
@SuppressLint("RestrictedApi")
private static <Key, Value> LiveData<PagedList<Value>> create(
@Nullable final Key initialLoadKey,
@NonNull final PagedList.Config config,
@Nullable final PagedList.BoundaryCallback boundaryCallback,
@NonNull final DataSource.Factory<Key, Value> dataSourceFactory,
@NonNull final Executor notifyExecutor,
@NonNull final Executor fetchExecutor) {
return new ComputableLiveData<PagedList<Value>>(fetchExecutor) {
@Nullable
private PagedList<Value> mList;
@Nullable
private DataSource<Key, Value> mDataSource;
private final DataSource.InvalidatedCallback mCallback =
new DataSource.InvalidatedCallback() {
@Override
public void onInvalidated() {
invalidate();
}
};
@SuppressWarnings("unchecked") // for casting getLastKey to Key
@Override
protected PagedList<Value> compute() {
// ·······
}
}.getLiveData();
}
??create
方法里面看似代碼非常多且復(fù)雜宫补,實(shí)際上就是創(chuàng)建ComputableLiveData
對象檬姥,然后獲取了ComputableLiveData
里面的LiveData。
??從名字上來看粉怕,我們都以為ComputableLiveData
是LiveData的實(shí)現(xiàn)類健民,實(shí)際上不是的;ComputableLiveData
可以理解為LiveData
的包裝類贫贝。那么ComputableLiveData
里面都封裝了啥玩意呢秉犹?我們可以簡單的看一下ComputableLiveData
的源碼:
@VisibleForTesting
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;
while (mInvalid.compareAndSet(true, false)) {
computed = true;
value = compute();
}
if (computed) {
mLiveData.postValue(value);
}
} finally {
// release compute lock
mComputing.set(false);
}
}
// check invalid after releasing compute lock to avoid the following scenario.
// Thread A runs compute()
// Thread A checks invalid, it is false
// Main thread sets invalid to true
// Thread B runs, fails to acquire compute lock and skips
// Thread A releases compute lock
// We've left invalid in set state. The check below recovers.
} while (computed && mInvalid.get());
}
};
// invalidation check always happens on the main thread
@VisibleForTesting
final Runnable mInvalidationRunnable = new Runnable() {
@MainThread
@Override
public void run() {
boolean isActive = mLiveData.hasActiveObservers();
if (mInvalid.compareAndSet(false, true)) {
if (isActive) {
mExecutor.execute(mRefreshRunnable);
}
}
}
};
??簡單來說,ComputableLiveData
的核心就是兩個(gè)Runnable:mInvalidationRunnable
和mRefreshRunnable
稚晚。
- mInvalidationRunnable:通過調(diào)用
ComputableLiveData
的invalidate
方法會(huì)執(zhí)行這個(gè)Runnable崇堵。這個(gè)Runnable內(nèi)部本身沒有承載很多的功能,就是簡單的判斷了一下狀態(tài)客燕,然后執(zhí)行mRefreshRunnable
來刷新數(shù)據(jù)鸳劳。mInvalidationRunnable
存在的意義就是為我們提供刷新的操作,比如說我們通過下拉刷新想要刷新當(dāng)前的數(shù)據(jù)也搓,應(yīng)該怎么怎么實(shí)現(xiàn)呢赏廓?我們都是通過調(diào)用DataSource的invalidate
方法來實(shí)現(xiàn),而DataSource的invalidate
方法就會(huì)回調(diào)到ComputableLiveData
的invalidate
方法傍妒,進(jìn)而實(shí)現(xiàn)刷新邏輯幔摸。至于為啥如此回調(diào),大家可以看一下上面create方法中的InvalidatedCallback
的實(shí)現(xiàn)颤练。- mRefreshRunnable:
mRefreshRunnable
的實(shí)現(xiàn)比mInvalidationRunnable
比較復(fù)雜一點(diǎn)既忆,但是不管怎么復(fù)雜,實(shí)際就是調(diào)用compute
方法創(chuàng)建一個(gè)PagedList對象嗦玖。
??我們來compute方法的實(shí)現(xiàn)患雇,看看它是怎么創(chuàng)建PagedList對象的:
protected PagedList<Value> compute() {
@Nullable Key initializeKey = initialLoadKey;
if (mList != null) {
initializeKey = (Key) mList.getLastKey();
}
do {
if (mDataSource != null) {
mDataSource.removeInvalidatedCallback(mCallback);
}
// 創(chuàng)建DataSource對象。
mDataSource = dataSourceFactory.create();
mDataSource.addInvalidatedCallback(mCallback);
// 創(chuàng)建PagedList踏揣。
mList = new PagedList.Builder<>(mDataSource, config)
.setNotifyExecutor(notifyExecutor)
.setFetchExecutor(fetchExecutor)
.setBoundaryCallback(boundaryCallback)
.setInitialKey(initializeKey)
.build();
} while (mList.isDetached());
return mList;
}
??我可以compute
方法的實(shí)現(xiàn)分為兩步:
- 創(chuàng)建DataSource對象庆亡。在這里,我們可以看到調(diào)用
DataSource.Factory
的create
方法創(chuàng)建了DataSource捞稿;同時(shí)又谋,從這里,我們可以知道每次刷新娱局,DataSource對象都會(huì)重新創(chuàng)建彰亥,所以大家在使用Paging時(shí),千萬不要嘗試在DataSource.Factory
里面復(fù)用DataSource
對象衰齐。- 通過
PagedList.Builder
創(chuàng)建一個(gè)PagedList對象任斋。
??到這里,我們并沒有看到調(diào)用數(shù)據(jù)加載的方法耻涛。我們進(jìn)一步往下看废酷,看一下PagedList.Builder
的build方法:
@WorkerThread
@NonNull
public PagedList<Value> build() {
// TODO: define defaults, once they can be used in module without android dependency
if (mNotifyExecutor == null) {
throw new IllegalArgumentException("MainThreadExecutor required");
}
if (mFetchExecutor == null) {
throw new IllegalArgumentException("BackgroundThreadExecutor required");
}
//noinspection unchecked
return PagedList.create(
mDataSource,
mNotifyExecutor,
mFetchExecutor,
mBoundaryCallback,
mConfig,
mInitialKey);
}
??build
方法并沒有做啥事瘟檩,只是調(diào)用了PagedList
的create方法,我們來看看create方法的實(shí)現(xiàn)(不得不吐槽澈蟆,這調(diào)用棧太深了...):
static <K, T> PagedList<T> create(@NonNull DataSource<K, T> dataSource,
@NonNull Executor notifyExecutor,
@NonNull Executor fetchExecutor,
@Nullable BoundaryCallback<T> boundaryCallback,
@NonNull Config config,
@Nullable K key) {
if (dataSource.isContiguous() || !config.enablePlaceholders) {
int lastLoad = ContiguousPagedList.LAST_LOAD_UNSPECIFIED;
if (!dataSource.isContiguous()) {
//noinspection unchecked
dataSource = (DataSource<K, T>) ((PositionalDataSource<T>) dataSource)
.wrapAsContiguousWithoutPlaceholders();
if (key != null) {
lastLoad = (Integer) key;
}
}
ContiguousDataSource<K, T> contigDataSource = (ContiguousDataSource<K, T>) dataSource;
return new ContiguousPagedList<>(contigDataSource,
notifyExecutor,
fetchExecutor,
boundaryCallback,
config,
key,
lastLoad);
} else {
return new TiledPagedList<>((PositionalDataSource<T>) dataSource,
notifyExecutor,
fetchExecutor,
boundaryCallback,
config,
(key != null) ? (Integer) key : 0);
}
}
??在create方法里面墨辛,我們可以發(fā)現(xiàn),這里通過一定的條件來判斷是創(chuàng)建ContiguousPagedList
還是TiledPagedList
趴俘。這個(gè)條件主要從兩個(gè)方面考慮:
- DataSource是否支持連續(xù)的數(shù)據(jù)睹簇,通過
isContiguous
方法來判斷。通過上面的內(nèi)容寥闪,我們知道ItemKeyedDataSource
和PageKeyedDataSource
都是連續(xù)的太惠。- 如果config里面配置不支持占位符,表示DataSource支持連續(xù)的數(shù)據(jù)疲憋。如果DataSource本身不支持連續(xù)的數(shù)據(jù)凿渊,那么就通過
wrapAsContiguousWithoutPlaceholders
方法將DataSource轉(zhuǎn)換成支持連續(xù)性數(shù)據(jù)的DataSource。也就是說柜某,如果我們使用的是PositionalDataSource
嗽元,但是在config配置了不支持占位符,那么就DataSource轉(zhuǎn)換為支持連續(xù)性數(shù)據(jù)的DataSource喂击。
??create方法的實(shí)現(xiàn)主要涉及到上面的兩點(diǎn),看上去實(shí)現(xiàn)沒有啥問題淤翔,但是我不得不吐槽一下:
- create方法是在
PagedList
里面翰绊。PagedList
作為父類,還要關(guān)心子類的實(shí)現(xiàn)旁壮,這個(gè)設(shè)計(jì)我覺得有待商榷的监嗜,這里完全可以使用工廠模式或者建造者模式來創(chuàng)建對象,而不是在父類里面創(chuàng)建子類對象抡谐。- 如果config里面配置了不支持占位符裁奇,就將DataSource變?yōu)檫B續(xù)性的。這個(gè)坑麦撵,我相信大家都多多少少的躺過刽肠,我不得不吐槽,為啥要這樣的設(shè)計(jì)免胃。對外的實(shí)現(xiàn)不透明固然是好的音五,但是這里總感覺是為了實(shí)現(xiàn)占位符的功能,而挖了大坑羔沙。在這種情況下躺涝,非連續(xù)的DataSource不支持占位符完全可以拋異常,而不是兼容...不知道Google爸爸是怎么想的扼雏。
??吐槽歸吐槽坚嗜,我們還是繼續(xù)的看一下兩個(gè)PagedList構(gòu)造方法的實(shí)現(xiàn)夯膀,先來看看ContiguousPagedList
:
ContiguousPagedList(
@NonNull ContiguousDataSource<K, V> dataSource,
@NonNull Executor mainThreadExecutor,
@NonNull Executor backgroundThreadExecutor,
@Nullable BoundaryCallback<V> boundaryCallback,
@NonNull Config config,
final @Nullable K key,
int lastLoad) {
super(new PagedStorage<V>(), mainThreadExecutor, backgroundThreadExecutor,
boundaryCallback, config);
mDataSource = dataSource;
mLastLoad = lastLoad;
if (mDataSource.isInvalid()) {
detach();
} else {
mDataSource.dispatchLoadInitial(key,
mConfig.initialLoadSizeHint,
mConfig.pageSize,
mConfig.enablePlaceholders,
mMainThreadExecutor,
mReceiver);
}
mShouldTrim = mDataSource.supportsPageDropping()
&& mConfig.maxSize != Config.MAX_SIZE_UNBOUNDED;
}
??其他地方我們不用關(guān)心,我們可以看到在這里調(diào)用了DataSource
的dispatchLoadInitial
方法苍蔬,這個(gè)方法就是用來請求第一頁的數(shù)據(jù)棍郎。我們來看看它的實(shí)現(xiàn),這里以ItemKeyedDataSource
為例:
@Override
final void dispatchLoadInitial(@Nullable Key key, int initialLoadSize, int pageSize,
boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> receiver) {
LoadInitialCallbackImpl<Value> callback =
new LoadInitialCallbackImpl<>(this, enablePlaceholders, receiver);
loadInitial(new LoadInitialParams<>(key, initialLoadSize, enablePlaceholders), callback);
// If initialLoad's callback is not called within the body, we force any following calls
// to post to the UI thread. This constructor may be run on a background thread, but
// after constructor, mutation must happen on UI thread.
callback.mCallbackHelper.setPostExecutor(mainThreadExecutor);
}
??dispatchLoadInitial
方法做的是比較簡單银室,就是創(chuàng)建了一個(gè)LoadInitialCallbackImpl
涂佃,然后就是調(diào)用loadInitial
方法進(jìn)行請求數(shù)據(jù),這個(gè)方法也是我們在自定義DataSource時(shí)必須重寫和實(shí)現(xiàn)的方法蜈敢。
??在這里辜荠,最最關(guān)鍵的一個(gè)一步就是調(diào)用setPostExecutor
方法設(shè)置mainThreadExecutor
。有人疑惑這個(gè)到底有啥用抓狭?這個(gè)就得從loadInitial
本身的實(shí)現(xiàn)說起伯病,相信大家都有一個(gè)疑惑,就是我們在這方法執(zhí)行網(wǎng)絡(luò)到底應(yīng)該放在子線程否过,還是保持該方法的原線程呢午笛?從Okhttp層面上來說,我們是應(yīng)該直接調(diào)用execute
還是enqueue
方法呢苗桂?
??從這個(gè)方法本身的注解來看药磺,我們直接通過execute
就行了,因?yàn)樵摲椒ǖ膱?zhí)行本身就放在子線程里面的:
@WorkerThread
public abstract void loadRange(@NonNull LoadRangeParams params,
@NonNull LoadRangeCallback<T> callback);
??而我想說的是煤伟,其實(shí)兩種方式都是可以癌佩,就是因?yàn)檎{(diào)用了setPostExecutor
方法。從兩個(gè)方面來分析一下這個(gè)問題:
- 不切換線程便锨。如果我們不切換線程围辙,那么
loadInitial
方法就是阻塞型,必須等網(wǎng)絡(luò)請求完成之后放案,才能保證PagedList創(chuàng)建成功姚建。也就是說,PagedListAdapter的submitList方法會(huì)等待到網(wǎng)絡(luò)請求才會(huì)回調(diào)吱殉,同時(shí)保證了提交的PagedList是肯定有數(shù)據(jù)的掸冤。- 切換線程。
loadInitial
方法就不是阻塞型的考婴,那么肯定在網(wǎng)絡(luò)請求完成之前,setPostExecutor
會(huì)被調(diào)用贩虾,那么請求會(huì)的數(shù)據(jù)也會(huì)通過mainThreadExecutor
對象post到主線程,從而保證Adapter的notifyXXX方法在主線程被調(diào)用沥阱。這種情況缎罢,需要特別注意的是submitList
方法被回調(diào)時(shí),提交的PagedList是一個(gè)空數(shù)據(jù)的數(shù)組。
??我記得在Google的Demo--PagingWithNetworkSample(現(xiàn)在是paging3了)里面策精,既有子線程調(diào)用的樣例舰始,也有主線程的樣例,其實(shí)都是可以的咽袜。對此丸卷,大家不用再存疑。
??我們自定義loadInitial
方法询刹,會(huì)將請求完成的結(jié)果通過callback
的onResult方法回調(diào)過來谜嫉,比如說,如下的代碼:
@WorkerThread
override fun loadInitial(
params: LoadInitialParams,
callback: LoadInitialCallback<Message>
) {
val execute = getService().getMessage(params.pageSize, 0).execute()
val messageList = execute.body()
val errorBody = execute.errorBody()
if (execute.code() == 200 && messageList != null && errorBody == null) {
callback.onResult(messageList, 0, Int.MAX_VALUE)
} else {
callback.onResult(Collections.emptyList(), 0)
}
}
??那么為什么必須要調(diào)用onResult
方法呢凹联?onResult
方法里面到底做什么啥事呢沐兰?今天我們看一下LoadInitialCallbackImpl
的onResult
方法的實(shí)現(xiàn):
public void onResult(@NonNull List<T> data, int position) {
if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
if (position < 0) {
throw new IllegalArgumentException("Position must be non-negative");
}
if (data.isEmpty() && position != 0) {
throw new IllegalArgumentException(
"Initial result cannot be empty if items are present in data set.");
}
if (mCountingEnabled) {
throw new IllegalStateException("Placeholders requested, but totalCount not"
+ " provided. Please call the three-parameter onResult method, or"
+ " disable placeholders in the PagedList.Config");
}
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
}
}
??LoadInitialCallbackImpl
存在兩個(gè)onResult
方法,其中如果在Config中開啟了執(zhí)行占位符蔽挠,最好是調(diào)用帶totalCount
的onResult
住闯;反之,則調(diào)用另一個(gè)onResult澳淑。我相信比原,大家對此也有疑問,本文在后面介紹Config的配置時(shí)杠巡,會(huì)重點(diǎn)介紹量窘,這里就先不贅述。
??回調(diào)最終會(huì)走到LoadCallbackHelper
的dispatchResultToReceiver
方法里面忽孽,我們來看看:
void dispatchResultToReceiver(final @NonNull PageResult<T> result) {
Executor executor;
synchronized (mSignalLock) {
if (mHasSignalled) {
throw new IllegalStateException(
"callback.onResult already called, cannot call again.");
}
mHasSignalled = true;
executor = mPostExecutor;
}
if (executor != null) {
executor.execute(new Runnable() {
@Override
public void run() {
mReceiver.onPageResult(mResultType, result);
}
});
} else {
mReceiver.onPageResult(mResultType, result);
}
}
??在這里绑改,我們可以看到mPostExecutor
的身影, 這就是前面通過setPostExecutor
方法設(shè)置的兄一,不過這些都不重要∈锻龋回調(diào)最后會(huì)走到PageResult.Receiver
的onPageResult
出革,那么onPageResult
方法里面做了啥事呢?
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) {
// ······
List<V> page = pageResult.page;
if (resultType == PageResult.INIT) {
// 將數(shù)據(jù)存儲(chǔ)到mStorage
// ······
} else {
// 將數(shù)據(jù)存儲(chǔ)到mStorage
// ······
if (mShouldTrim) {
// 裁剪數(shù)據(jù)渡讼。
}
}
// ······
}
};
??onPageResult
方法看上去挺復(fù)雜的骂束,其實(shí)就只做了兩件事:
- 將數(shù)據(jù)存儲(chǔ)到
mStorage
中去,主要是區(qū)分了三種情況:INIT表示第一次加載數(shù)據(jù)成箫;APPEND表示加載下一頁的數(shù)據(jù)展箱;PREPEND表示加載上一頁的數(shù)據(jù)。- 裁剪數(shù)據(jù)蹬昌。有人可能會(huì)有疑問混驰,為啥會(huì)有裁剪數(shù)據(jù)的操作,什么才叫裁剪數(shù)據(jù)呢?這個(gè)先要介紹一下
PagedStorage
這個(gè)類栖榨。顧名思義昆汹,PagedStorage
的作用就是存儲(chǔ)數(shù)據(jù)的,用什么樣的數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)數(shù)據(jù)呢婴栽?分頁加載當(dāng)然就是一頁一頁的存儲(chǔ)满粗,所以數(shù)據(jù)結(jié)構(gòu)就是類似于ArrayList<ArrayList<Data>>
。PagedStorage
內(nèi)部便是這樣的實(shí)現(xiàn)愚争,裁剪數(shù)據(jù)的目的將一些沒必要的數(shù)據(jù)裁剪掉映皆,比如說,某些用于占位符的數(shù)據(jù)轰枝,在PagedStorage
內(nèi)部就是一個(gè)PLACEHOLDER_LIST
對象捅彻,還就是裁剪一些某些為null數(shù)據(jù),在Config里面有一個(gè)mMaxSize
的配置項(xiàng)狸膏,我們可以通過設(shè)置具體的數(shù)目沟饥,但是設(shè)置了這么了一定的數(shù)目,那么沒有還沒有加載的數(shù)據(jù)怎么表示呢湾戳?PagedStorage
會(huì)通過null表示贤旷。這個(gè)我們可以從Adpater的getItemCount方法得到一定的答案。假設(shè)我們設(shè)置為1000砾脑,那么getItemCount
方法肯定返回1000幼驶,沒有加載的數(shù)據(jù)都是用null來表示的。mMaxSize
這個(gè)配置項(xiàng)實(shí)際上比較復(fù)雜韧衣,我們后面重點(diǎn)介紹盅藻。
??至此,我們對加載第一頁數(shù)據(jù)的邏輯已經(jīng)理解的差不多了畅铭。簡單的來說氏淑,就是在創(chuàng)建PagedList的時(shí)候會(huì)進(jìn)行請求。我們需要注意的是硕噩,在loadInitial
方法里面假残,區(qū)分異步加載和同步加載的不同點(diǎn)。
(2). 加載下一頁數(shù)據(jù)
??由于ContiguousDataSource
存在dispatchLoadAfter
和dispatchLoadBefore
兩個(gè)不同的加載邏輯炉擅,這里我將這兩個(gè)方法加載的數(shù)據(jù)統(tǒng)稱為加載下一頁數(shù)據(jù)辉懒。
??PagedListAdapter
在通過getItem
方法回去對應(yīng)位置的數(shù)據(jù)時(shí),會(huì)有一個(gè)特殊的調(diào)用谍失,我們來看看具體的代碼--AsyncPagedListDiffer
的getItem
方法:
public T getItem(int index) {
// ······
mPagedList.loadAround(index);
// ······
}
??下一頁數(shù)據(jù)的加載就是通過這里來觸發(fā)的眶俩,我們來看看具體的實(shí)現(xiàn):
public void loadAround(int index) {
if (index < 0 || index >= size()) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size());
}
mLastLoad = index + getPositionOffset();
loadAroundInternal(index);
mLowestIndexAccessed = Math.min(mLowestIndexAccessed, index);
mHighestIndexAccessed = Math.max(mHighestIndexAccessed, index);
/*
* mLowestIndexAccessed / mHighestIndexAccessed have been updated, so check if we need to
* dispatch boundary callbacks. Boundary callbacks are deferred until last items are loaded,
* and accesses happen near the boundaries.
*
* Note: we post here, since RecyclerView may want to add items in response, and this
* call occurs in PagedListAdapter bind.
*/
tryDispatchBoundaryCallbacks(true);
}
??loadAround
方法本身沒有做多少事,真正的操作都在loadAroundInternal
方法里面快鱼,我們來看看具體的實(shí)現(xiàn)颠印,這里以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();
}
}
??loadAroundInternal
方法做的事實(shí)際上非常的簡單纲岭,就是判斷調(diào)用schedulePrepend
方法還是scheduleAppend
方法,主要是通過設(shè)置的預(yù)取距離跟當(dāng)前所處的位置對比嗽仪。這兩個(gè)就是用來分別觸發(fā)DataSource的dispatchLoadAfter
方法和dispatchLoadBefore
方法荒勇。
??那么,這兩個(gè)方法最后調(diào)用到哪里呢闻坚?其實(shí)就是我們在自定義DataSource重寫的兩個(gè)方法:loadAfter
和loadBefore
沽翔。在這里,我們需要的是注意的這兩個(gè)方法是在子線程里面調(diào)用的窿凤,同時(shí)仅偎,在創(chuàng)建LoadCallbackImpl
時(shí)還設(shè)置了mainThreadExecutor
:
@Override
final void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem,
int pageSize, @NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> receiver) {
loadBefore(new LoadParams<>(getKey(currentBeginItem), pageSize),
new LoadCallbackImpl<>(this, PageResult.PREPEND, mainThreadExecutor, receiver));
}
??跟loadInitial
方法不一樣的是,在loadBefore
方法調(diào)用的時(shí)候,mainThreadExecutor
已經(jīng)不為null了雳殊,所以在loadBefore
方法中橘沥,不要使用異步方法進(jìn)行求網(wǎng)絡(luò)請求,主要出于如下兩個(gè)方面考慮:
- 盡量減少異步線程的數(shù)量夯秃。
loadBefore
方法本身就在子線程里面調(diào)用的座咆,我們沒有必要再去啟動(dòng)線程,我們都知道系統(tǒng)的資源都是有限的仓洼,啟動(dòng)一個(gè)線程還是比較消耗系統(tǒng)資源的介陶。- 避免出現(xiàn)一些奇怪的問題。子線程里面再去啟一個(gè)子線程色建,最后的回調(diào)接口最初的主線程里面哺呜,中間垮了兩個(gè)線程,這個(gè)過程極易容易出現(xiàn)線程安全問題箕戳。
4. PagedList的Config配置
??相信大家才開始使用的PagedList的時(shí)候某残,在Config配置上踩了很多的坑。今天陵吸,我就在這里重點(diǎn)介紹每個(gè)配置的作用玻墅。
字段名稱 | 解釋 |
---|---|
mPageSize | 每頁的大小,主要透傳到請求方法里面壮虫,用來決定請求數(shù)據(jù)的數(shù)量椭豫。 |
mPrefetchDistance | 預(yù)取范圍,用來設(shè)置滑動(dòng)什么位置才請求下一頁的數(shù)據(jù)旨指。 |
mInitialLoadSizeHint | 初始化請求數(shù)據(jù)的數(shù)量。 |
mEnablePlaceholders | 是否開啟占位符喳整,true表示開啟谆构,false則表示不開啟。 |
mMaxSize | 數(shù)據(jù)的總數(shù)框都。 |
??上面簡單的介紹了一下每個(gè)字段含義搬素,接下來我們將詳細(xì)的解釋每個(gè)字段的作用。
(1). mPageSize
??其實(shí)我們從這個(gè)名字里面就可以知道,這個(gè)字段的含義就表示每頁的大小熬尺,其實(shí)我們在進(jìn)行網(wǎng)絡(luò)請求的請求時(shí)摸屠,也完全沒必要通過mPageSize
字段決定請求數(shù)據(jù)的數(shù)量。
??針對于兩個(gè)實(shí)現(xiàn)不同的DataSource粱哼,對mPageSize字段應(yīng)用的程度也是不同的季二。其中ContiguousPagedList
沒有對mPageSize
做過多的要求,包括我們在請求數(shù)據(jù)的時(shí)候揭措,也可以忽略這個(gè)字段(雖然可以這么做胯舷,但是最好別這樣做)。
??而TiledPagedList
對mPageSize
則是強(qiáng)依賴绊含,從兩個(gè)方面來說:
??首先桑嘶,從初始化請求方面說起,在計(jì)算初始化的數(shù)量時(shí)躬充,會(huì)通過mPageSize
來計(jì)算:
@WorkerThread
TiledPagedList(@NonNull PositionalDataSource<T> dataSource,
@NonNull Executor mainThreadExecutor,
@NonNull Executor backgroundThreadExecutor,
@Nullable BoundaryCallback<T> boundaryCallback,
@NonNull Config config,
int position) {
// ······
if (mDataSource.isInvalid()) {
// ······
} else {
final int firstLoadSize =
(Math.max(mConfig.initialLoadSizeHint / pageSize, 2)) * pageSize;
final int idealStart = position - firstLoadSize / 2;
final int roundedPageStart = Math.max(0, idealStart / pageSize * pageSize);
mDataSource.dispatchLoadInitial(true, roundedPageStart, firstLoadSize,
pageSize, mMainThreadExecutor, mReceiver);
}
}
??通過上面的代碼逃顶,我們可以發(fā)現(xiàn),TiledPagedList
會(huì)將初始化頁面大小設(shè)置為mPageSize
的整倍數(shù)充甚。
??需要特別注意的是:我們在loadInitial
方法設(shè)置請求數(shù)量以政,必須是mPageSize
的整數(shù)倍。因?yàn)槲覀兛梢詮?code>onResult方法里面看到一個(gè)判斷:
@Override
public void onResult(@NonNull List<T> data, int position, int totalCount) {
if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
LoadCallbackHelper.validateInitialLoadParams(data, position, totalCount);
if (position + data.size() != totalCount
&& data.size() % mPageSize != 0) {
throw new IllegalArgumentException("PositionalDataSource requires initial load"
+ " size to be a multiple of page size to support internal tiling."
+ " loadSize " + data.size() + ", position " + position
+ ", totalCount " + totalCount + ", pageSize " + mPageSize);
}
if (mCountingEnabled) {
int trailingUnloadedCount = totalCount - position - data.size();
mCallbackHelper.dispatchResultToReceiver(
new PageResult<>(data, position, trailingUnloadedCount, 0));
} else {
// Only occurs when wrapped as contiguous
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
}
}
}
??那么Google爸爸為啥要千方百計(jì)的保證請求數(shù)據(jù)的數(shù)量是pageSize的整數(shù)倍呢津坑?這個(gè)其實(shí)跟占位符有關(guān)妙蔗,一旦開啟了占位符,Google爸爸就認(rèn)為每一頁請求都是應(yīng)該是一樣的疆瑰,所以當(dāng)遇到不同的size時(shí)眉反,比如說在初始化時(shí)時(shí)整數(shù)倍的pageSize,Google爸爸就會(huì)進(jìn)行分頁穆役。
??比如說寸五,mPageSize
為20,第一次請求的數(shù)據(jù)有40條耿币;那么就會(huì)把這40條數(shù)據(jù)拆分成為兩頁的數(shù)據(jù)梳杏,那么在哪里進(jìn)行拆分的呢?就在PagedStorage
的initAndSplit
方法里面:
void initAndSplit(int leadingNulls, @NonNull List<T> multiPageList,
int trailingNulls, int positionOffset, int pageSize, @NonNull Callback callback) {
int pageCount = (multiPageList.size() + (pageSize - 1)) / pageSize;
for (int i = 0; i < pageCount; i++) {
int beginInclusive = i * pageSize;
int endExclusive = Math.min(multiPageList.size(), (i + 1) * pageSize);
List<T> sublist = multiPageList.subList(beginInclusive, endExclusive);
if (i == 0) {
// Trailing nulls for first page includes other pages in multiPageList
int initialTrailingNulls = trailingNulls + multiPageList.size() - sublist.size();
init(leadingNulls, sublist, initialTrailingNulls, positionOffset);
} else {
int insertPosition = leadingNulls + beginInclusive;
insertPage(insertPosition, sublist, null);
}
}
callback.onInitialized(size());
}
??initAndSplit
方法很簡單,就是數(shù)據(jù)拆分為一頁的一頁的存儲(chǔ)起來淹接,保證每頁大小都是我們設(shè)置的mPageSize十性。
??其次,再來看看加載下一頁的數(shù)據(jù)的請求塑悼,當(dāng)請求到下一頁的數(shù)據(jù)劲适,會(huì)通過PagedStorage
的insertPage
方法存儲(chǔ)起來,在insertPage方法里面有一個(gè)特別的判斷:
public void insertPage(int position, @NonNull List<T> page, @Nullable Callback callback) {
final int newPageSize = page.size();
if (newPageSize != mPageSize) {
// differing page size is OK in 2 cases, when the page is being added:
// 1) to the end (in which case, ignore new smaller size)
// 2) only the last page has been added so far (in which case, adopt new bigger size)
int size = size();
boolean addingLastPage = position == (size - size % mPageSize)
&& newPageSize < mPageSize;
boolean onlyEndPagePresent = mTrailingNullCount == 0 && mPages.size() == 1
&& newPageSize > mPageSize;
// OK only if existing single page, and it's the last one
if (!onlyEndPagePresent && !addingLastPage) {
throw new IllegalArgumentException("page introduces incorrect tiling");
}
if (onlyEndPagePresent) {
mPageSize = newPageSize;
}
}
// ······
}
??如果我們請求的數(shù)據(jù)大小不符合要求厢蒜,直接回拋出異常霞势。那么什么是不符合要求呢烹植?就是請求返回的不為mPageSize。
??那么為什么必須要保證每頁是一樣的愕贡,這里我就簡單的介紹一下草雕,感興趣的可以看看PagedStorage
的實(shí)現(xiàn):
當(dāng)我們的Adpter通過getItem方法獲取數(shù)據(jù)時(shí),其實(shí)調(diào)用的是
PagedStorage
的get方法獲取固以。我們知道分頁數(shù)據(jù)其實(shí)是通過數(shù)組包裹數(shù)組的數(shù)據(jù)結(jié)構(gòu)進(jìn)行存儲(chǔ)數(shù)據(jù)的墩虹,所以在獲取數(shù)據(jù)時(shí),需要獲取兩個(gè)Index嘴纺,在PagedStorage
內(nèi)部稱為localIndex
和pageInternalIndex
,這兩個(gè)index一個(gè)是一維數(shù)組的index败晴,一個(gè)是二維數(shù)組的index。如果每頁大小都是一樣的(這種情況在PagedStorage
內(nèi)部被稱為Tiled
)栽渴,那么就可以通過如下方式如下計(jì)算:
// it's inside mPages, and we're tiled. Jump to correct tile.
localPageIndex = localIndex / mPageSize;
pageInternalIndex = localIndex % mPageSize;
所以尖坤,這就是為啥要保證每頁大小必須一樣的原因。
??上面介紹那么多闲擦,總結(jié)起來就是:初始化請求時(shí)慢味,請求數(shù)據(jù)的總數(shù)必須是mPageSize的整數(shù)倍,加載下一頁時(shí)必須為mPageSize墅冷。簡而言之纯路,我們在請求傳參的時(shí)候,不要亂搞寞忿,避免出現(xiàn)各種問題驰唬,建議都傳mPageSize,即param里面帶的那個(gè)Size腔彰。
(2). mPrefetchDistance
??mPrefetchDistance
表示也是非常的簡單叫编,就是表示預(yù)取距離姥份,比如說勾怒,我們設(shè)置為5项乒,就表示滑動(dòng)到倒數(shù)第5個(gè)的時(shí)候肠虽,我們才請求下一頁的數(shù)據(jù)。
??我們先來看看ContiguousPagedList
的應(yīng)用:
@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();
}
}
??ContiguousPagedList
就在loadAroundInternal
方法里面進(jìn)行判斷的冒掌,具體的細(xì)節(jié)這里我們就不深入的討論了歪今。
??我們再來看看PositionalDataSource
的實(shí)現(xiàn):
public void allocatePlaceholders(int index, int prefetchDistance,
int pageSize, Callback callback) {
if (pageSize != mPageSize) {
if (pageSize < mPageSize) {
throw new IllegalArgumentException("Page size cannot be reduced");
}
if (mPages.size() != 1 || mTrailingNullCount != 0) {
// not in single, last page allocated case - can't change page size
throw new IllegalArgumentException(
"Page size can change only if last page is only one present");
}
mPageSize = pageSize;
}
final int maxPageCount = (size() + mPageSize - 1) / mPageSize;
int minimumPage = Math.max((index - prefetchDistance) / mPageSize, 0);
int maximumPage = Math.min((index + prefetchDistance) / mPageSize, maxPageCount - 1);
allocatePageRange(minimumPage, maximumPage);
int leadingNullPages = mLeadingNullCount / mPageSize;
for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) {
int localPageIndex = pageIndex - leadingNullPages;
if (mPages.get(localPageIndex) == null) {
//noinspection unchecked
mPages.set(localPageIndex, PLACEHOLDER_LIST);
callback.onPagePlaceholderInserted(pageIndex);
}
}
}
??ContiguousPagedList
就在PagedStorage
的allocatePlaceholders
方法里面進(jìn)行判斷的萧锉,具體的細(xì)節(jié)這里我們就不深入的討論了端逼。說一句題外話朗兵,PagedStorage
很重要,如果想要理解PagedList的機(jī)制顶滩,一定要了解它矛市,有機(jī)會(huì)我會(huì)專門的寫一篇文章來分析它。
(4).mInitialLoadSizeHint
??這個(gè)字段的含義也非常的簡單诲祸,就是初始化請求數(shù)據(jù)的大小浊吏。這里需要特別的是:ContiguousPagedList
的大小是設(shè)置的多少,請求數(shù)據(jù)時(shí)拿到就是多少救氯;TiledPagedList
的大小要根據(jù)mInitialLoadSizeHint
設(shè)置的大小而定找田,如果mInitialLoadSizeHint
比mPageSize
大,那么就是 2 * mPageSize着憨。
(5). mEnablePlaceholders
??這個(gè)字段表示的含義就是是否開啟占位符墩衙,意思看上去非常的簡單,但是Paging的內(nèi)部使用這個(gè)字段貫穿全文甲抖。相信大家在使用Paging的時(shí)候都有一個(gè)問題漆改,就是當(dāng)我們init方法里面回調(diào)結(jié)果時(shí),應(yīng)該調(diào)用帶totalCount的onResult
方法准谚,還是不帶toltalCount的onResult
方法挫剑?
??同時(shí),大家在使用PositionalDataSource
時(shí)柱衔,發(fā)現(xiàn)將mEnablePlaceholders
設(shè)置為true樊破,此時(shí)只能調(diào)用帶totalCountonResult
方法。在以前的版本中唆铐,這里調(diào)用錯(cuò)了哲戚,頁面什么反應(yīng)都沒有,現(xiàn)在還好艾岂,會(huì)拋異常了顺少。這又是為啥呢?
??當(dāng)我們不開啟占位符時(shí),為啥不能用TiledPagedList
?我們在PagedList
的create方法中發(fā)現(xiàn)王浴,當(dāng)沒有開啟占位符脆炎,盡管我們使用的是PositionalDataSource
,最后是還是會(huì)使用wrapAsContiguousWithoutPlaceholders
方法將PositionalDataSource
轉(zhuǎn)換成為連續(xù)的DataSource叼耙,創(chuàng)建的PagedList也是ContiguousPagedList
腕窥。
??接下來的內(nèi)容,我們將一一的解答上面三個(gè)問題筛婉。
??回到這個(gè)字段的本身簇爆,開啟占位符到底表示什么意思,可以看一下下面的效果圖:
??占位符的意思非常簡單爽撒,就是指有些Item的內(nèi)容還沒有加載回來入蛆,先用一些默認(rèn)的UI來表示,比如說硕勿,上圖中顯示
加載中
就是表示沒有數(shù)據(jù)還沒有加載回來哨毁。??所以,在這里源武,我們可以解釋上面的第三個(gè)問題扼褪。當(dāng)我們沒有開啟占位符的時(shí)候想幻,Adapter通過getItem方法獲取的數(shù)據(jù)肯定不為空,所以可以認(rèn)為每一頁的每一項(xiàng)數(shù)據(jù)都是有效话浇,且是完整的脏毯,這個(gè)就比較符合連續(xù)性的數(shù)據(jù)的邏輯,同時(shí)連續(xù)的數(shù)據(jù)方便維護(hù)幔崖,因?yàn)檫B續(xù)的數(shù)據(jù)通常不用進(jìn)行trim食店,更不會(huì)使用null和類似于
PLACEHOLDER_LIST
這種來表示占位,所以將其轉(zhuǎn)換成為連續(xù)的數(shù)據(jù)類型是簡化實(shí)現(xiàn)赏寇。??接下來吉嫩,我們來看一下兩個(gè)
onResult
方法。熟悉Paging 的同學(xué)應(yīng)該都知道嗅定,如果我們開啟了占位符自娩,一定要調(diào)用帶totalCount的方法?事實(shí)真是如此的嗎露戒?這里分別從ContiguousDataSource
和PositionalDataSource
來看下椒功。??在
ContiguousDataSource
及其子類中,我們會(huì)發(fā)現(xiàn)onResult
方法一共如下兩個(gè):
public abstract void onResult(@NonNull List<Value> data);
public abstract void onResult(@NonNull List<Value> data, int position, int totalCount);
??其實(shí)智什,在ContiguousDataSource
內(nèi)部动漾,不管是否開啟占位符,帶totalCount
的onResult
方法都可以調(diào)用荠锭,只是有一定區(qū)別:
totalCount表示的意思旱眯,我們可以簡單的理解為當(dāng)前數(shù)據(jù)的總數(shù)。
- 當(dāng)開啟開啟了占位符证九。調(diào)用帶
totalCount
的onResult
方法删豺,就表示當(dāng)前數(shù)據(jù)總數(shù)一定為totalCount
,Adapter的itemCount也會(huì)是totalCount
愧怜,此時(shí)getItem獲取的數(shù)據(jù)可能為空呀页;如果調(diào)用的是不帶totalCount
的onResult
方法,那么Adapter的itemCount就是具體數(shù)據(jù)的數(shù)量拥坛,此時(shí)getItem獲取的肯定不為空蓬蝶。- 當(dāng)沒有開啟占位符。兩個(gè)
onResult
方法沒有區(qū)別猜惋。
??我們再來看看 PositionalDataSource
,在其內(nèi)部兩個(gè)onResult
方法的定義如下:
public abstract void onResult(@NonNull List<T> data, int position);
public abstract void onResult(@NonNull List<T> data, int position, int totalCount);
??我們分別來看看這個(gè)兩個(gè)方法的區(qū)別:
- 當(dāng)開啟了占位符丸氛,只有調(diào)用帶
totalCount
的方法,調(diào)用另一個(gè)方法直接拋異常著摔。需要特別注意的是缓窜,此時(shí)totalCount傳遞的最大值為Int.MAX_VALUE - params.pageSize
,如下的代碼:
@WorkerThread
override fun loadInitial(
params: LoadInitialParams,
callback: LoadInitialCallback<Message>
) {
val execute = RequestUtils.getService().getMessage(params.pageSize, 0).execute()
val messageList = execute.body()
val errorBody = execute.errorBody()
if (execute.code() == 200 && messageList != null && errorBody == null) {
callback.onResult(messageList, 0, Int.MAX_VALUE - params.pageSize)
} else {
callback.onResult(Collections.emptyList(), 0)
}
}
??因?yàn)槿绻覀儌鬟fInteger.MAX_VALUE
,在加載下一頁數(shù)據(jù)的時(shí)候禾锤,PagedStorage
計(jì)算數(shù)據(jù)時(shí)會(huì)溢出私股,這也是為什么當(dāng)我們傳遞Integer.MAX_VALUE
,下一頁的數(shù)據(jù)沒有成功加載时肿,溢出代碼如下:
public void allocatePlaceholders(int index, int prefetchDistance,
int pageSize, Callback callback) {
// ······
// 這里會(huì)溢出
final int maxPageCount = (size() + mPageSize - 1) / mPageSize;
int minimumPage = Math.max((index - prefetchDistance) / mPageSize, 0);
int maximumPage = Math.min((index + prefetchDistance) / mPageSize, maxPageCount - 1);
allocatePageRange(minimumPage, maximumPage);
int leadingNullPages = mLeadingNullCount / mPageSize;
for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) {
int localPageIndex = pageIndex - leadingNullPages;
if (mPages.get(localPageIndex) == null) {
//noinspection unchecked
mPages.set(localPageIndex, PLACEHOLDER_LIST);
callback.onPagePlaceholderInserted(pageIndex);
}
}
}
- 當(dāng)沒有開啟占位符庇茫,兩個(gè)方法沒有區(qū)別。
(6). mMaxSize
??這個(gè)字段表示的意思雖然是數(shù)據(jù)的總數(shù)螃成,但是實(shí)際上,這個(gè)字段極其的坑人查坪。我們在使用這個(gè)字段時(shí)寸宏,發(fā)現(xiàn)不僅不會(huì)生效,而且設(shè)置了之后還是出現(xiàn)各種問題:
- 假設(shè)我們在使用
ContiguousDataSource
設(shè)置為100,沒有開啟占位符的話偿曙,會(huì)出現(xiàn)混亂的問題氮凝,具體效果如下:
開啟占位符的話,mMaxSize不生效望忆,具體的效果如下:
- 假設(shè)我們在使用
ContiguousDataSource
設(shè)置為100罩阵,沒有開啟占位符的效果跟ContiguousDataSource
,數(shù)據(jù)會(huì)混亂启摄;開啟占位符的話稿壁,mMaxSize就生效了,這也是唯一生效的地方歉备,具體效果:
??簡單的來說傅是,這個(gè)配置極其的不好用,同時(shí)Google爸爸在方法上也進(jìn)行了特別的注釋蕾羊,Google爸爸說:mMaxSize只能盡力而為喧笔,不能百分百的保證」暝伲可想而知书闸,這個(gè)配置是多么的雞肋。
??需要特別的注意的是:在mMaxSize
唯一生效的地方利凑,如果我們設(shè)置的mMaxSize
和totalCount是不一樣的值浆劲,那么就以totalCount
為準(zhǔn)。
??所以截碴,如果我們要限制大小的話梳侨,最好是自己來實(shí)現(xiàn),不要使用這個(gè)字段日丹。
5. 總結(jié)
??到這里走哺,本文的內(nèi)容就到此結(jié)束了,其實(shí)關(guān)于pagin的內(nèi)容不僅僅是這些哲虾,本文的內(nèi)容只能說起到一個(gè)提綱挈領(lǐng)的作用丙躏,比如說择示,PagedStorage
的設(shè)計(jì),這部分內(nèi)容并沒有深入的介紹晒旅,有興趣的同學(xué)可以去看看栅盲,我相信大家理解這個(gè)類所做的事,對Paging
的理解會(huì)更加深入废恋。最后我來簡單的總結(jié)一下本文的內(nèi)容:
- 在Paging中谈秫,我們可以
PagedList
和DataSource
分為兩類:非連續(xù)的和連續(xù)的,兩者其實(shí)沒有本質(zhì)上的區(qū)別鱼鼓,只是在一些特殊業(yè)務(wù)場景上可能會(huì)有一點(diǎn)區(qū)別拟烫,比如說占位符,非連續(xù)的數(shù)據(jù)如果沒有開啟占位符的特性迄本,其實(shí)本質(zhì)上跟連續(xù)的數(shù)據(jù)是一樣的硕淑。- 初始化請求數(shù)據(jù),是在PagedList的構(gòu)造方法里面進(jìn)行嘉赎,其中初始化請求方法本身在子線程里面執(zhí)行置媳,所以我們直接使用同步方法進(jìn)行網(wǎng)絡(luò)請求即可(當(dāng)然也可以使用異步方法,但是不推薦公条。)拇囊;下一頁數(shù)據(jù)的請求時(shí)機(jī),是在getItem方法里面的觸發(fā)赃份,PagedList會(huì)根據(jù)position來決定是否請求下一頁的數(shù)據(jù)寂拆。
- Config的
mPageSize
用來限制每頁數(shù)據(jù)的大小,同時(shí)我們在網(wǎng)絡(luò)請求時(shí)抓韩,一定要使用給定的size纠永,不要想著搞各種騷操作,避免出現(xiàn)各種問題谒拴。- Config中的
mEnablePlaceholders
用來控制是否使用占位符尝江。ContiguousDataSource
和PositionalDataSource
對于開啟占位符有不同的要求。ContiguousDataSource
在網(wǎng)絡(luò)請求回調(diào)的時(shí)候英上,兩個(gè)onResult
方法都可以使用炭序,本質(zhì)上并沒有什么區(qū)別,只是要注意的是當(dāng)調(diào)用帶toltalCount
的onResult
方法是苍日,getItem可能返回為null惭聂,這個(gè)在使用的時(shí)候需要特別關(guān)心;PositionalDataSource
開啟了占位符相恃,只能調(diào)用帶toltalCount
的onResult
方法。- 如果使用的是
PositionalDataSource
,onResult
方法中的toltalCount
的值不要超過Integer.MAX_VALUE - pageSize
,因?yàn)樵谟?jì)算位置的時(shí)候可能會(huì)溢出,導(dǎo)致不能加載下一頁的數(shù)據(jù)耕腾。- Config中的
mMaxSize
用來限制總數(shù)據(jù)的大小见剩,但是實(shí)際上作用范圍非常的小,只在PositionalDataSource
開啟占位符才生效扫俺,同時(shí)如果toltalCount
跟mMaxSize
不一樣的苍苞,會(huì)以toltalCount
為準(zhǔn)±俏常總之來說羹呵,不要輕易的使用mMaxSize
。
??最后疗琉,我想簡單的說幾句話担巩,paging是為了解決分頁加載的問題而出現(xiàn),這個(gè)初衷是很好的没炒,但是使用的門檻實(shí)在是太高了,稍稍不注意就可能出現(xiàn)出錯(cuò)誤犯戏,比如說Config的配置送火,onResult的回調(diào)。同時(shí)先匪,我覺得paging在代碼設(shè)計(jì)上也有一定的問題种吸,比如說區(qū)分連續(xù)和非連續(xù)的,這個(gè)直接導(dǎo)致實(shí)現(xiàn)DataSource和PagedList的工作量翻倍呀非;PagedStorage
將各種代碼和實(shí)現(xiàn)糅合在一個(gè)類里面坚俗,導(dǎo)致閱讀起來特別費(fèi)勁。不過最近有一個(gè)好消息的是岸裙,Google爸爸在最新的JetPack推出了paging3猖败,我希望這些問題都已經(jīng)解決了。