Paging分页加载库源码分析

来新公司半年多,最近一直在参与 Andorid 团队的架构升级工作。最近在图片选择库中使用了 paging 作为分页加载框架。顺便阅读了一下paging的源码。在这里记录一下。

初次接除 paging, 可能会一脸懵逼,感觉出来了很多 API, 不知道从哪里下手。我们先对 paging 的组成部分进行一个了解。

首先,我们按照 列表分页加载 这个行为进行一个基本的划分,分为 2 个部分, 数据UI, paging 就是按照这个来进行划分的

数据

数据部分 paging 包括

UI

UI 部分 paging 提供了一个新的 PagedListAdapter, 在实例化这个 Adapter 的时候,我们需要提供一个自己实现的 DiffUtil.ItemCallback 或者 AsyncDifferConfig

入门

以分页数据源 PageKeyedDataSource 为例

创建一个数据源, 其中 Language 为 demo 中的实体对象

class LanguageDataSource: PageKeyedDataSource<Int, Language>()

实现三个 override 方法

override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Language>) {
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Language>) {
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Language>) {
}

着 3 个方法,依次解释为

  • 初次加载
  • 后面一页加载
  • 前一页加载

我们给第一页数据填充逻辑

LanguageRepository.requestLanguages({datas->
    if (datas.code == 200) {
        val languages = datas.data
        Handler(Looper.getMainLooper()).post {
           callback.onResult(languages, null, 1)
        }
    } else {
    }
 }, {t->
     Log.e(javaClass.simpleName, "${t.message}")
})

其中 LanguageRepository 是利用 retrofit 请求了一个 Language 对象的列表。 我们调用
callback.onResult 就会刷新 RecyclerView 的视图

loadAfter 的实现大致与 loadInitial 一致,这里不做赘述。

我们再来看一下 UI 层,我们定义一个 PagedListAdapter

class LanguageAdapter(private val context: Context) : PagedListAdapter<Language, ViewHolder>(languageDiff)

这里我们需要 override 2个方法

 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder
override fun onBindViewHolder(holder: ViewHolder, position: Int)

onBindViewHolder 中, 我们可以通过 getItem(position) 获取相对于的数据实例去进行 UI 的展示。

接下来是一个比较关键的部分,那就是如何连接 DATA 和 UI 这两部分。

val config = PagedList.Config.Builder()
    .setPageSize(15)
    .setPrefetchDistance(2)
    .setInitialLoadSizeHint(15)
    .setEnablePlaceholders(false)
    .build()

val pageList = PagedList.Builder(LanguageDataSource(), config)
    .setNotifyExecutor {
         Handler(Looper.getMainLooper()).post {it.run()}
    }
    .setFetchExecutor(Executors.newFixedThreadPool(2))
    .build()

 adapter.submitList(pageList)

在这里, pageList 的 NotifyExecutorFetchExecutor 也是必须设置的。在 Android arch componet 完整的架构中,更推荐使用构建一个 PageListLiveData 的方式。但是不使用也没有关系,arch compoent 的完整内容在这里不做过多的描述。具体的详细使用可以查看google的实例源码

在大致了解了 paging 的组成部分后,我们会开始好奇,那我们到底为什么需要 paging 呢, 他和我们之前普通的使用方式有什么区别呢,我们可以在源码中寻找到答案。

我们可以在 2 个部分的真正对接处作为切入点进行分析,查看 PagedList.Builder#build() 的源码:

return PagedList.create(
    mDataSource,
    mNotifyExecutor,
    mFetchExecutor,
    mBoundaryCallback,
    mConfig,
    mInitialKey);

继续查看

return new ContiguousPagedList<>(contigDataSource,
    notifyExecutor,
    fetchExecutor,
    boundaryCallback,
    config,
    key,
    lastLoad);

跟到这个类的构造方法,可以看到如下逻辑

mDataSource.dispatchLoadInitial(key,
    mConfig.initialLoadSizeHint,
    mConfig.pageSize,
    mConfig.enablePlaceholders,
    mMainThreadExecutor,
    mReceiver);

这里以 PageKeyedDataSource 为例, 其他的 DataSource 对象同理

查看 dispatchLoadInital 方法

LoadInitialCallbackImpl<Key, Value> callback =
                new LoadInitialCallbackImpl<>(this, enablePlaceholders, receiver);
loadInitial(new LoadInitialParams<Key>(initialLoadSize, enablePlaceholders), callback);

callback.mCallbackHelper.setPostExecutor(mainThreadExecutor);

这里我们可以看到, loadInitial 就是我们需要在 override 的方法之一。那我们里面调用 callback 的 onResult 方法到底发生了什么呢?

查看 LoadInitialCallbackImpl#onResult() 的源码,关键逻辑如下

mDataSource.initKeys(previousPageKey, nextPageKey);
int trailingUnloadedCount = totalCount - position - data.size();
if (mCountingEnabled) {
    mCallbackHelper.dispatchResultToReceiver(new PageResult<>(
    data, position, trailingUnloadedCount, 0));
} else {
    mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
}

查看 dispatchResultToReceiver

image

继续查看 onPageResult 方法

image

我们关注一下 init 时候的逻辑

mStorage.init(pageResult.leadingNulls, page, pageResult.trailingNulls,
                        pageResult.positionOffset, ContiguousPagedList.this);

init 的逻辑很简单,只有 2 行

init(leadingNulls, page, trailingNulls, positionOffset);
callback.onInitialized(size());
image

在这里, 我们可以看见关键的逻辑

mPages.clear();
mPages.add(page);

这里,和 PageList 绑定的数据就发生了变化。之后我们把 PageList submit 给了 adapter 那么,数据就发生了更新。

初始加载我们看完了,那么,剩下的数据是如何加载的呢

我们反过来看 RecyclerView, 如果我们滑动列表或者其他操作的时候,很自然会调用 adapter 的 bind 方法。那么,我们去查看 PagedListAdapter#getItem 的源码。

return mDiffer.getItem(position);
image

查看 PageListloadAround

loadAroundInternal(index);

继续,

image
if (mAppendItemsRequested > 0) {
    scheduleAppend();
}

查看 scheduleAppend 的实现

mBackgroundThreadExecutor.execute(new Runnable() {
            @Override
            public void run() {
                if (isDetached()) {
                    return;
                }
                if (mDataSource.isInvalid()) {
                    detach();
                } else {
                    mDataSource.dispatchLoadAfter(position, item, mConfig.pageSize,
                            mMainThreadExecutor, mReceiver);
                }
            }
        });

这里,我们看到了 dispatchLoadAfter 方法的调用,之后的逻辑和之前的 dispathLoadInitial 就非常的类似了。

最终,会调用到如下逻辑


image

这里会走 AsyncPagedListDifferPagedList.Callback 的回调

image

这里,callback 是和 adapter 关联起来的。所以会在这里刷新列表。

最后,我们看一下 Adapter 的 submit 方法,最后可以看到这样的逻辑

image

我们可以看到 paging 是利用了 DiffUtils 对 RecyclerView 进行刷新的。这样我们也无需担心 paging 会存在性能问题。

理解

最后谈一下对 paging 的理解。 一般情况下,我们最原始的方式,列表 UI 所在的部分,是需要知道数据的来源等逻辑部分,我们在常见的 mvp 模式中,会对数据和 UI 进行分层。 而 paging 就利用一系列的封装, 提供了更加通用的 API 调用来做这些事情。更通俗点说,就是实现了分页加载结构中的 Presenter 层及 Presenter层的下游处理部分。

这种模式,业务的编写者,可以把 UI 部分的代码模板化, 只需要关心业务逻辑,并且把业务逻辑中的数据获取写在 DataSource 中,使分页加载的操作解耦程度更高。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容