列表预加载分析研究

当今移动运用开发中,列表控件是最常用的UI控件之一,它能够显现各种信息,如图片、文本、视频等等。然而,在移动设备上,列表数据的加载和显现是十分消耗资源的操作。当列表中的数据量较大时,用户往往需求等候较长的时间才干看到完整的列表。为了进步用户体会,开发人员需求采纳一些战略来减少加载时间,如预加载。
预加载是指在用户滑动列表之前,提早加载一部分列表数据,以便在用户滑动到这些数据时能够当即显现,然后进步用户的体会和感知速度。Android系统供给了一些API和技能来完成列表预加载,本课题旨在对Android列表预加载进行深化研究和分析,探究其完成原理、优化战略和功用影响,为开发人员供给参考和辅导。

接下来咱们就深化研究几种预加载列表的办法,它们或是经过 Android 系统的 API 扩展或是各种第三方结构完备的完成,了解它们的完成原理,方便咱们根据详细的事务需求和功用要求进行综合评价和比较,然后选择最合适的计划来完成列表预加载功用。

RecyclerView.OnScrollListener

经过 RecyclerView 的 addOnScrollListener 接口,咱们能够监听到 RecyclerView 的滑动状况,然后经过该 RecyclerView 装载的 LayoutManager 来得到当时滑动最下方或最上方展现的 item 的索引,痛殴索引来判别咱们是否需求执行预加载逻辑。

rvList.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        // 获取 LayoutManger
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        // 假如 LayoutManager 是 LinearLayoutManager
        if (layoutManager instanceof GridLayoutManager) {
            GridLayoutManager manager = (GridLayoutManager) layoutManager;
            int nextPreloadCount = 8;
            int previousPreloadCount = 4;
            if (dy > 0
                    && manager.findLastVisibleItemPosition()
                    == layoutManager.getItemCount() - 1 - nextPreloadCount) {
                mViewModel.loadNext();
            } else if (dy > 0 && manager.findFirstVisibleItemPosition() == previousPreloadCount) {
                mViewModel.loadPrevious();
            }
        }
    }
});

经过给列表页的 RecyerView 增加这一段代码,咱们就能够很轻松的完成列表的预加载,在有本地本地数据缓存排除网络状况的状况下,和之前经过结构的 loadMore 触发有着清楚明了的不同。但于此一起,在检查日志是就很简略发现,onScrolled 办法会在一次滑动中屡次重复调用,形成同一个页面加载的屡次调用,网络加载下对功用以及数据流量有很大的影响,需求事务逻辑上做去重处理。

定论

RecyclerView.onScrollListener 的完成和集成办法大致如上,经过实践咱们能够得出曾经定论:

长处

  • 编码简略
  • 代码入侵性低,无需修正现有的 RecyclerView 或 adapter

缺陷

  • onScrolled 办法会在一次滑动中屡次重复调用,需求事务逻辑自行做去重判别
  • 不同的 LayoutManager 会有不同的判别逻辑,需求不断的兼容扩展

Adapter.onBindViewHolder

咱们监听列表的原因是想知道当时滑动到第几项目,然后来决议是否要开端预加载,为此需求拿到滑动的状况和 LayoutManager . 实践上 Adapter 就有天生的简略易用的回调,那就是 onBindViewHolder onBindViewHolderRecyclerView 需求显现指定的 Position 的 数据时才会通知,这时咱们就能够根据 BindViewHolderPosition 以及整个列表的数据对比来判别咱们是否需求进行预加载,而无需实时的关心列表的滑动状况和 LayoutManager 的类型

接下来咱们就运用 Adpater 的 onBindViewHolder 来完成以下预加载:

public class ListAdapter extends RecyclerView.Adapter<ViewHolder> {
	private int nextPreloadCount = 8;
	private int previousPreloadCount = 4;
	private boolean isScroll
	public void onBindViewHolder(@NonNull VH holder, int position,
                @NonNull List<Object> payloads) {
			checkPreload(position);
  }
	public void bindRecyelerView(@NonNull RecyclerView recyclerView ) {
		 if (newState == RecyclerView.SCROLL_STATE_IDLE) {
         isScroll = false;
     } else {
			 	 isScroll = true;
		 }
	}
	private void checkPreload(int position) {
		 if (!isScroll){
				return;
		 }
     if (position == previousPreloadCount) {
          mViewModel.loadPrevious();
     } else if (position == getItemCount() - 1 - nextPreloadCount) {
          mViewModel.loadNext();
     }
    }
}

代码全体完成也简略易懂,列表滑动进程中所有的列表项的加载都会经过 Adapter.onBindViewHolder
然后触发预加载检测共同,因为 RecyclerView 的逻辑处理,onBindViewHolder 不会存在单次滑动中被屡次调用的状况。且因为 Adpater 的 position 获取是与 layoutManger 无关的,所以也不需求 layoutManger 相关的代码逻辑。但是这个计划也依然优缺陷,那就是当触发预加载的 viewHolder 在列表加载进程中被向上滑出了 RecyclerView 的缓存区域时,再向下滑动到页尾时会再次被绑定导致 onBindViewHolder 触发,然后使得预加载重复触发。

定论

Adapter.onBindViewHolder 的完成也并不杂乱,且相较于监听列表的完成差异显着

长处

  • 代码编写逻辑简略,基类可多处复用,与类型无关
  • RecyclerView 对页首页尾的 ViewHolder 并不会当即收回,不会在正常的滑动事情内触发屡次加载

缺陷

  • 仍会重新触发上拉加载,还是需求做去重操作。

BaseRecyclerViewAdapterHelpr

BaseRecyclerViewAdapterHelpr 是一个强大而灵敏的 RecyclerView Adapter ,是一个在 github 具有 23.4 K star 的库,许多商业项意图的 adapter 都会采用它,以下简称 BRVAH 。 咱们来探究下它是怎么处理预加载计划的,经过文档检查咱们发现它的加载更多的逻辑是专门有 QuickAdapterHelper.kt 来完成的,直接检查它的预加载计划 :

加载上一页

leadingLoadStateAdapter?.let {
            mAdapter.addAdapter(it)
            firstAdapterOnViewAttachChangeListener =
                object : BaseQuickAdapter.OnViewAttachStateChangeListener {
                    override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
                        leadingLoadStateAdapter.checkPreload(holder.bindingAdapterPosition)
                    }
                    override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
                    }
                }.apply { contentAdapter.addOnViewAttachStateChangeListener(this) }

加载下一页

trailingLoadStateAdapter?.let {
            mAdapter.addAdapter(it)
            lastAdapterOnViewAttachChangeListener =
                object : BaseQuickAdapter.OnViewAttachStateChangeListener {
                    override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
                        trailingLoadStateAdapter.checkPreload(
                            holder.bindingAdapter?.itemCount ?: 0,
                            holder.bindingAdapterPosition
                        )
                    }
                    override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
                    }
                }.apply { contentAdapter.addOnViewAttachStateChangeListener(this) }
        }

BRVAH 全体对加载的计划采用的是 ConcatAdapter ,加载的头部和尾部经过独立的 adapter 来做逻辑操控,所以两部的代码根本共同,这儿咱们就拿加载下一页的逻辑来梳理。 contentAdapter 就是实践的列表 adapter ,trailingLoadStateAdapter 则是专门担任列表尾部逻辑处理的 adapter ,能够看到它创立了一个 OnViewAttachStateChangeListener 用来监听 viewHolder 的 onViewAttachedToWindow , 其详细完成是绑定了 Adapter 的 onViewAttachedToWindow ,经过这个要害触发预加载监测机制。详细完成就不在深究了,咱们知道它的触发要害和大致完成即可,感兴趣的能够直接去查阅对应的源码,其实践完成也并不杂乱。

能够看到 BRVAH 尽管采用了对 Adapter 进行封装处理预加载逻辑,但它并没有采用 Adpater 的 onBindViewHolder 当作触发要害而是采用了 onViewAttachedToWindow 形成这样的差异是什么,咱们能够看下这两者的实践不同:

onViewAttachedToWindow办法在RecyclerView中显现一个ViewHolder时被调用。当RecyclerView需求显现一个新的ViewHolder时,它会调用Adapter的onCreateViewHolder办法来创立一个ViewHolder,然后将这个ViewHolder绑定到数据源中对应的数据上,最后调用ViewHolder的onBindViewHolder办法将数据显现在ViewHolder的视图上。这时,假如ViewHolder被成功增加到RecyclerView中,onViewAttachedToWindow办法就会被调用。

因而,onViewAttachedToWindow办法在ViewHolder显现在RecyclerView上时触发,而**onBindViewHolder办法则是在RecyclerView需求更新ViewHolder数据时触发。

从咱们的事务场景出发—— 预加载的意图是经过滑动来判别用户或许有向下滑动的意图,提早补充列表数据,防止用户等候。事务场景其实并不太依靠当时视图是否真的展现在界面上了,所以这儿没有用生命周期更靠前的 onBindViewHolder 而用了更靠后的 onViewAttachedToWindow 从源码角度上来看并没有得到好的解说,去检查仓库也没有相关的提交注释。只能后续看是否能联系上作者询问了

定论

BRVAH 带了新的预加载计划,尽管目前看实质上与 onBindViewHolder 相似,但是BRVAH 除此之外还供给了成套的处理计划,包含防止重复加载以及列表头尾的优雅处理。

长处

  • 集成难度中等
  • 有较高的 star 和活跃度,出现问题的概率较小
  • 供给了成套的处理计划,防止造轮子

缺陷

  • 需求引进新的库,修正调整现有的 adapter

BRV

BRV 是一个根据 SmartRefreshLayout 结构的扩展库,他在 SmartRefreshLayout 的基础上供给了预加载,缺省页,悬停标题等功用,声称具有比 BRVAH 更强大的功用以及实用性,它的其间预加载逻辑如下:

/** 监听onBindViewHolder事情 */
    var onBindViewHolderListener = object : OnBindViewHolderListener {
        override fun onBindViewHolder(
            rv: RecyclerView,
            adapter: BindingAdapter,
            holder: BindingAdapter.BindingViewHolder,
            position: Int,
        ) {
            if (mEnableLoadMore && !mFooterNoMoreData &&
                rv.scrollState != SCROLL_STATE_IDLE &&
                preloadIndex != -1 &&
                (adapter.itemCount - preloadIndex <= position)
            ) {
                post {
                    if (state == RefreshState.None) {
                        notifyStateChanged(RefreshState.Loading)
                        onLoadMore(this@PageRefreshLayout)
                    }
                }
            }
        }
    }

能够看到与咱们编写 onBindViewHolder 的监听逻辑根本千篇一律,经过当时的触发的 bindViewHolder position 来判别是否要触发预加载,而加载的头部和尾部则是根据 SmartRefreshLayout 来的,经过 ViewGruop 独自的 add 增加和 remove 掉。

定论

BRV 的预加载计划根本与咱们自己根据 Adpater 的根本共同,在此基础上增加了去重处理

长处

  • 供给了成套的处理计划,防止数据重复造轮子

缺陷

  • 集成难度杂乱,依靠 SmartRefreshLayout 没有引进 SmartRefreshLayout 库的话还需求独自引进
  • 只供给了向后预加载,不支撑向前预加载

Paging 3

Paging 库概览 | Android 开发者 | Android Developers

Paging 作为 Jetpack 的组件,专门用于加载和显现来自本地和网络中的数据页面,相同也供给了数据预加载的功用,那么作为官方的列表加载计划,它又是怎么完成的。

Paging3 作为一整套的列表处理计划,它供给了分页数据的内存缓存、内置的请求重复信息删除功用 以及对刷新与重试功用的支撑等等,此外, paging3 还很多的运用了 Flow 作为数据处理完成,功用调用栈也极深。导致代码阅读杂乱较高,这儿咱们就只了解下 Paging3 预加载的要害以及判别逻辑,用于跟其他结构进行对比。

触发要害

首先是触发要害,Paging3 供给了 PagingDataAdapter 作为 RecyclerView 的适配器,开发者必须运用根据它的 Adapter 来进行列表适配,PagingDataAdapter 内置了 diff 机制以及直接办理列表数据,列表设置和更新需求经过 submitData 办法,获取数据则经过 getItem 办法,而 paging3 的预加载机制则就藏匿在 getItem 的详细完成中,因为列表数据是完全封装起来的,调用者只能经过 getItem 来获取列表数据,而调用 getItem 往往是在 onBindViewHolder 时,所以 paging3 的分页触发要害也根本等同于 onBindViewHolder 办法

判别逻辑

getItem 办法触发时,paging3 会生成 ViewportHint 的快照,用来存储描述当时列表的状况,一起根据这些信息来判别是否要触发预加载

/**
     * Processes the hint coming from UI.
     */
    fun processHint(viewportHint: ViewportHint) {
        state.modify(viewportHint as? ViewportHint.Access) { prependHint, appendHint ->
            if (viewportHint.shouldPrioritizeOver(
                    previous = prependHint.value,
                    loadType = PREPEND
                )
            ) {
                prependHint.value = viewportHint
            }
            if (viewportHint.shouldPrioritizeOver(
                    previous = appendHint.value,
                    loadType = APPEND
                )
            ) {
                appendHint.value = viewportHint
            }
        }
    }

prependHint, appendHint 实质也分别是一个 Flow ,当时契合预加载机制后,它们会将 viewportHint 发送到专门用于处理此类数据的 PageFetcherSnapshot 将来转换成一个刷新事情然后融入整个数据加载流程。

Paging3 的预加载机制大致就是如此,更详细的机制因为代码量太多不变深化,假如对 Paging3 不熟悉和感兴趣的能够放下边官方的 CodeLab 做深化了解

定论

paging3 的预加载只是整个库的冰山一角,但是由此也能够看到官方也是经过 onBindViewHodler 作为预加载的判别要害的,给咱们选择更轻量的计划做了一定的背书

长处

  • 有完备的机制以及官方背书,出现问题的概率较小
  • 预加载还加入了锁的处理,考虑了多线程并发,完全处理了或许出现的屡次请求问题。

缺陷

  • 集成难度十分大,paging3 是一整套列表处理计划,需求各个层级的逻辑变更
  • 代码由 kotlin 、Flow 以及协程编写,预读和调适性教差,Java 接入不友好

CodeLab

Google 供给了两个引导文档来让开发人员快速的学习怎么集成和运用 Paging 3

Android Paging 基础知识 | Android Developers

Android Paging Advanced Codelab | Android Developers

总结

在完成列表预加载的进程中,选择合适的技能计划十分要害,今日咱们介绍了以下几种列表预加载计划:

  1. 运用onScrollListener技能,能够经过监听翻滚事情,在滑动到指定位置之前提早加载数据,以此完成列表预加载的功用。它的完成简略,一起缺陷也适当显着。
  2. 运用 BindViewHolder 技能,能够在绑定 ViewHolder 时进行数据的预加载,以此进步列表数据的加载速度和用户体会。它的逻辑简略明了,也是许多处理计划的中心逻辑。假如考虑自己封装的话,那么以它为蓝本是不贰之选。
  3. BRV结构是一个开源的Android列表结构,它根据SmartRefreshLayout ,供给了许多常用的列表功用,包含列表预加载。BRV结构能够方便地完成列表预加载,并供给了许多其他的功用,如分组、拖拽等等。遗憾的是并不支撑列表向前预加载。
  4. Paging结构是一个Android官方供给的用于完成分页加载的结构。它能够方便地完成列表预加载,一起还供给了分页加载、数据缓存等功用。
  5. BaseRecyclerViewAdapterHelper是一个轻量级的RecyclerView适配器,它能够快速地构建RecyclerView 列表,并支撑列表预加载等功用。

选择哪种计划完成列表预加载,需求根据详细的事务需求和功用要求进行综合评价和比较,然后选择最合适的计划来完成列表预加载功用。

参考资料:

换一个思路,超简略的RecyclerView预加载 –

预加载/预拉取 – BRV

github.com/CymChad/Bas…

Paging | Android 开发者 | Android Developers