Jetpack 源碼分析(四) - Paging源碼分析

??距離上一篇Jetpack源碼分析的文章已經(jīng)兩個(gè)月铛绰,時(shí)間間隔確實(shí)有點(diǎn)長舞吭。最近绰上,感覺自己的學(xué)習(xí)積極性不那么的高旨怠,看Paging的源碼也是斷斷續(xù)續(xù)的。時(shí)至今日蜈块,才算是完成對Paging的源碼學(xué)習(xí)鉴腻。今天我們就來學(xué)習(xí)Paging的實(shí)現(xiàn)原理。
??本文參考資料:

  1. Paging library overviewg
  2. Display paged listsi
  3. Gather paged data
  4. 反思|Android 列表分頁組件Paging的設(shè)計(jì)與實(shí)現(xiàn):架構(gòu)設(shè)計(jì)與原理解析
  5. Android Jetpack- paging的基本使用

??注意百揭,本文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è)方面來看看:

  1. 耦合度比較高。OnScrollerListener在計(jì)算位置的時(shí)候氮双,通常來說會(huì)依賴RecyclerView的LayoutManager碰酝,不同LayoutManager有不同計(jì)算方式,如果后面RecyclerView有很多不同的LayoutManager戴差,OnScrollerListener里面就會(huì)變的非常復(fù)雜送爸。
  2. 擴(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喇喉、PagedListDataSource。這其中校坑,PagedListAdapterDataSource比較熟悉拣技,因?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ù)源的所有操作,其中包括:

  1. 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對象憔晒。
  2. 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)介紹。
  3. 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)是不一樣的:

  1. ContiguousPagedList關(guān)心的是onPagePrependedonPageAppended幔嫂,也就是說,連續(xù)的PagedList關(guān)心的是上一頁數(shù)據(jù)和下一頁數(shù)據(jù)的加載誊薄。同時(shí)我們從源碼可以簡單的看到履恩,類似于onPageInserted這類TiledPagedList比較關(guān)心的方法,在ContiguousPagedList的內(nèi)部是不支持的呢蔫。
  2. 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è)方面:

  1. DataSource的實(shí)現(xiàn)類比較多。跟PagedList比較類似箫津,DataSource也可以非分為連續(xù)的和非連續(xù)的狭姨;但是跟PagedList不一樣宰啦,每個(gè)部分的實(shí)現(xiàn)類均還有實(shí)現(xiàn)類(主要分頁加載的場景比較多。)饼拍。
  2. 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)系來的涵叮。

  1. 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ī)穆刻。
  2. PagedList:首先是給PagedListAdapter提供對應(yīng)的接口置尔,讓其能夠獲取數(shù)據(jù)以及加載下一頁的數(shù)據(jù);其次就是氢伟,直接持有DataSource的引用榜轿,可以直接對其進(jìn)行對應(yīng)的操作,比如說朵锣,加載數(shù)據(jù)等谬盐。
  3. DataSource:三兄弟中最底層和最累的一個(gè),主要是對PagedList提供接口诚些,讓其能夠進(jìn)行對應(yīng)的操作飞傀。

??到這里,我們對Paging庫里面基本組成部分有了一個(gè)大概的了解诬烹,接下來我們將從源碼角度來分析一下Paging的主要實(shí)現(xiàn)原理砸烦,本文主要從如下幾個(gè)方面來分析Paging:

  1. paging如何進(jìn)行初始化第一頁數(shù)據(jù)(類似于刷新)。
  2. paging如何加載下一頁的數(shù)據(jù)绞吁。
  3. 從源碼角度來分析 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:mInvalidationRunnablemRefreshRunnable稚晚。

  1. mInvalidationRunnable:通過調(diào)用ComputableLiveDatainvalidate方法會(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)到ComputableLiveDatainvalidate方法傍妒,進(jìn)而實(shí)現(xiàn)刷新邏輯幔摸。至于為啥如此回調(diào),大家可以看一下上面create方法中的InvalidatedCallback的實(shí)現(xiàn)颤练。
  2. mRefreshRunnablemRefreshRunnable的實(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)分為兩步:

  1. 創(chuàng)建DataSource對象庆亡。在這里,我們可以看到調(diào)用DataSource.Factorycreate方法創(chuàng)建了DataSource捞稿;同時(shí)又谋,從這里,我們可以知道每次刷新娱局,DataSource對象都會(huì)重新創(chuàng)建彰亥,所以大家在使用Paging時(shí),千萬不要嘗試在DataSource.Factory里面復(fù)用DataSource對象衰齐。
  2. 通過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è)方面考慮:

  1. DataSource是否支持連續(xù)的數(shù)據(jù)睹簇,通過isContiguous方法來判斷。通過上面的內(nèi)容寥闪,我們知道ItemKeyedDataSourcePageKeyedDataSource都是連續(xù)的太惠。
  2. 如果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)沒有啥問題淤翔,但是我不得不吐槽一下:

  1. create方法是在PagedList里面翰绊。PagedList作為父類,還要關(guān)心子類的實(shí)現(xiàn)旁壮,這個(gè)設(shè)計(jì)我覺得有待商榷的监嗜,這里完全可以使用工廠模式或者建造者模式來創(chuàng)建對象,而不是在父類里面創(chuàng)建子類對象抡谐。
  2. 如果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)用了DataSourcedispatchLoadInitial方法苍蔬,這個(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è)問題:

  1. 不切換線程便锨。如果我們不切換線程围辙,那么loadInitial方法就是阻塞型,必須等網(wǎng)絡(luò)請求完成之后放案,才能保證PagedList創(chuàng)建成功姚建。也就是說,PagedListAdapter的submitList方法會(huì)等待到網(wǎng)絡(luò)請求才會(huì)回調(diào)吱殉,同時(shí)保證了提交的PagedList是肯定有數(shù)據(jù)的掸冤。
  2. 切換線程。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方法里面到底做什么啥事呢沐兰?今天我們看一下LoadInitialCallbackImplonResult方法的實(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)用帶totalCountonResult住闯;反之,則調(diào)用另一個(gè)onResult澳淑。我相信比原,大家對此也有疑問,本文在后面介紹Config的配置時(shí)杠巡,會(huì)重點(diǎn)介紹量窘,這里就先不贅述。
??回調(diào)最終會(huì)走到LoadCallbackHelperdispatchResultToReceiver方法里面忽孽,我們來看看:

        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.ReceiveronPageResult出革,那么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í)就只做了兩件事:

  1. 將數(shù)據(jù)存儲(chǔ)到mStorage中去,主要是區(qū)分了三種情況:INIT表示第一次加載數(shù)據(jù)成箫;APPEND表示加載下一頁的數(shù)據(jù)展箱;PREPEND表示加載上一頁的數(shù)據(jù)。
  2. 裁剪數(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存在dispatchLoadAfterdispatchLoadBefore兩個(gè)不同的加載邏輯炉擅,這里我將這兩個(gè)方法加載的數(shù)據(jù)統(tǒng)稱為加載下一頁數(shù)據(jù)辉懒。
??PagedListAdapter在通過getItem方法回去對應(yīng)位置的數(shù)據(jù)時(shí),會(huì)有一個(gè)特殊的調(diào)用谍失,我們來看看具體的代碼--AsyncPagedListDiffergetItem方法:

    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è)方法:loadAfterloadBefore沽翔。在這里,我們需要的是注意的這兩個(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è)方面考慮:

  1. 盡量減少異步線程的數(shù)量夯秃。loadBefore方法本身就在子線程里面調(diào)用的座咆,我們沒有必要再去啟動(dòng)線程,我們都知道系統(tǒng)的資源都是有限的仓洼,啟動(dòng)一個(gè)線程還是比較消耗系統(tǒng)資源的介陶。
  2. 避免出現(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è)字段(雖然可以這么做胯舷,但是最好別這樣做)。
??而TiledPagedListmPageSize則是強(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)行拆分的呢?就在PagedStorageinitAndSplit方法里面:

    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ì)通過PagedStorageinsertPage方法存儲(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)部稱為localIndexpageInternalIndex,這兩個(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就在PagedStorageallocatePlaceholders方法里面進(jìn)行判斷的萧锉,具體的細(xì)節(jié)這里我們就不深入的討論了端逼。說一句題外話朗兵,PagedStorage很重要,如果想要理解PagedList的機(jī)制顶滩,一定要了解它矛市,有機(jī)會(huì)我會(huì)專門的寫一篇文章來分析它。

(4).mInitialLoadSizeHint

??這個(gè)字段的含義也非常的簡單诲祸,就是初始化請求數(shù)據(jù)的大小浊吏。這里需要特別的是:ContiguousPagedList的大小是設(shè)置的多少,請求數(shù)據(jù)時(shí)拿到就是多少救氯;TiledPagedList的大小要根據(jù)mInitialLoadSizeHint設(shè)置的大小而定找田,如果mInitialLoadSizeHintmPageSize大,那么就是 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í)真是如此的嗎露戒?這里分別從ContiguousDataSourcePositionalDataSource來看下椒功。
??在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)部动漾,不管是否開啟占位符,帶totalCountonResult方法都可以調(diào)用荠锭,只是有一定區(qū)別:

totalCount表示的意思旱眯,我們可以簡單的理解為當(dāng)前數(shù)據(jù)的總數(shù)。

  1. 當(dāng)開啟開啟了占位符证九。調(diào)用帶totalCountonResult方法删豺,就表示當(dāng)前數(shù)據(jù)總數(shù)一定為totalCount,Adapter的itemCount也會(huì)是totalCount愧怜,此時(shí)getItem獲取的數(shù)據(jù)可能為空呀页;如果調(diào)用的是不帶totalCountonResult方法,那么Adapter的itemCount就是具體數(shù)據(jù)的數(shù)量拥坛,此時(shí)getItem獲取的肯定不為空蓬蝶。
  2. 當(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ū)別:

  1. 當(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);
            }
        }
    }
  1. 當(dāng)沒有開啟占位符庇茫,兩個(gè)方法沒有區(qū)別。

(6). mMaxSize

??這個(gè)字段表示的意思雖然是數(shù)據(jù)的總數(shù)螃成,但是實(shí)際上,這個(gè)字段極其的坑人查坪。我們在使用這個(gè)字段時(shí)寸宏,發(fā)現(xiàn)不僅不會(huì)生效,而且設(shè)置了之后還是出現(xiàn)各種問題:

  1. 假設(shè)我們在使用ContiguousDataSource設(shè)置為100,沒有開啟占位符的話偿曙,會(huì)出現(xiàn)混亂的問題氮凝,具體效果如下:

    開啟占位符的話,mMaxSize不生效望忆,具體的效果如下:
    demo.gif
  2. 假設(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)容:

  1. 在Paging中谈秫,我們可以PagedListDataSource分為兩類:非連續(xù)的和連續(xù)的,兩者其實(shí)沒有本質(zhì)上的區(qū)別鱼鼓,只是在一些特殊業(yè)務(wù)場景上可能會(huì)有一點(diǎn)區(qū)別拟烫,比如說占位符,非連續(xù)的數(shù)據(jù)如果沒有開啟占位符的特性迄本,其實(shí)本質(zhì)上跟連續(xù)的數(shù)據(jù)是一樣的硕淑。
  2. 初始化請求數(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ù)寂拆。
  3. Config的mPageSize用來限制每頁數(shù)據(jù)的大小,同時(shí)我們在網(wǎng)絡(luò)請求時(shí)抓韩,一定要使用給定的size纠永,不要想著搞各種騷操作,避免出現(xiàn)各種問題谒拴。
  4. Config中的mEnablePlaceholders用來控制是否使用占位符尝江。ContiguousDataSourcePositionalDataSource對于開啟占位符有不同的要求。ContiguousDataSource在網(wǎng)絡(luò)請求回調(diào)的時(shí)候英上,兩個(gè)onResult方法都可以使用炭序,本質(zhì)上并沒有什么區(qū)別,只是要注意的是當(dāng)調(diào)用帶toltalCountonResult方法是苍日,getItem可能返回為null惭聂,這個(gè)在使用的時(shí)候需要特別關(guān)心;PositionalDataSource開啟了占位符相恃,只能調(diào)用帶toltalCountonResult方法。
  5. 如果使用的是PositionalDataSource,onResult方法中的toltalCount的值不要超過Integer.MAX_VALUE - pageSize,因?yàn)樵谟?jì)算位置的時(shí)候可能會(huì)溢出,導(dǎo)致不能加載下一頁的數(shù)據(jù)耕腾。
  6. Config中的mMaxSize用來限制總數(shù)據(jù)的大小见剩,但是實(shí)際上作用范圍非常的小,只在PositionalDataSource開啟占位符才生效扫俺,同時(shí)如果toltalCountmMaxSize不一樣的苍苞,會(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)解決了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末降允,一起剝皮案震驚了整個(gè)濱河市恩闻,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌剧董,老刑警劉巖幢尚,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異翅楼,居然都是意外死亡尉剩,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門毅臊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來理茎,“玉大人,你說我怎么就攤上這事」︱眩” “怎么了园爷?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長式撼。 經(jīng)常有香客問我童社,道長,這世上最難降的妖魔是什么著隆? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任扰楼,我火速辦了婚禮,結(jié)果婚禮上美浦,老公的妹妹穿的比我還像新娘弦赖。我一直安慰自己,他們只是感情好浦辨,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布蹬竖。 她就那樣靜靜地躺著,像睡著了一般流酬。 火紅的嫁衣襯著肌膚如雪币厕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天芽腾,我揣著相機(jī)與錄音旦装,去河邊找鬼。 笑死摊滔,一個(gè)胖子當(dāng)著我的面吹牛阴绢,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播艰躺,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼呻袭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了描滔?” 一聲冷哼從身側(cè)響起棒妨,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎含长,沒想到半個(gè)月后券腔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拘泞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年纷纫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陪腌。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡辱魁,死狀恐怖烟瞧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情染簇,我是刑警寧澤参滴,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站锻弓,受9級(jí)特大地震影響砾赔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜青灼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一暴心、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧杂拨,春花似錦专普、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至策橘,卻和暖如春击胜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背役纹。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留暇唾,地道東北人促脉。 一個(gè)月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像策州,于是被迫代替她去往敵國和親瘸味。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354