ViewPager2系列:

  1. 图解RecyclerView缓存复用机制
  2. 图解RecyclerView预拉取机制
  3. 图解ViewPager2离屏加载机制(上)

前言

在前两篇文章中,咱们经过一张张明晰明晰的「示意图」,具体地复盘了RecyclerView「缓存复用机制」与「预拉取机制」的作业流程,这种「图解」创造形式也得到了来自不同平台读者们的共同认可。

而从本文开端,咱们将正式进入ViewPager2的华章,并将辅以愈加生动易懂的「动态示意图」来进行讲解。

ViewPager2可讲的内容有很多,今天咱们首要介绍是ViewPager2的「离屏加载机制」,你可能是第一次传闻这个术语,但在实践开发中,你肯定运用过它,因为它对应的配置入口,就是ViewPager2的OffscreenPageLimit属性。

OffscreenPageLimit是什么?

OffscreenPageLimit,直译过来是离屏页面约束值的意思,该值代表的是在滑动视图中应保存在当时可见页面之外的任一方向上的页面数

比如,当咱们选用水平分页时,该值代表的便是在左右两边应保存的页面数。

【动画图解】这个值取对了,ViewPager2才能纵享丝滑

而当咱们选用笔直分页时,该值代表的则是在上下两边应保存的页面数。

【动画图解】这个值取对了,ViewPager2才能纵享丝滑

保存页面的办法是经过扩展额定的布局空间实现的,以LinearLayoutManager为例,其最要害的步骤在于对calculateExtraLayoutSpace办法的重写:

    /**
    * 核算额定的布局空间
    */
    @Override
    protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,
            @NonNull int[] extraLayoutSpace) {
        int pageLimit = getOffscreenPageLimit();
        if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) {
            // 仅在需求时才对屏幕外页面进行自定义预取
            super.calculateExtraLayoutSpace(state, extraLayoutSpace);
            return;
        }
        // 核算多pageLimit*2个页面巨细的空间
        final int offscreenSpace = getPageSize() * pageLimit;
        extraLayoutSpace[0] = offscreenSpace;
        extraLayoutSpace[1] = offscreenSpace;
    }
    /**
    * 获取单个页面巨细
    */
    int getPageSize() {
        final RecyclerView rv = mRecyclerView;
        // 水平分页时,取去除了左右内边距后的RecyclerView宽度
        // 笔直分页时,取去除了上下内边距后的RecyclerView高度
        return getOrientation() == ORIENTATION_HORIZONTAL
                ? rv.getWidth() - rv.getPaddingLeft() - rv.getPaddingRight()
                : rv.getHeight() - rv.getPaddingTop() - rv.getPaddingBottom();
    }

该办法会核算LinearLayoutManager应安置的额定空间量(以像素为单位)。已知默许安置的空间量为单个页面巨细,则额定安置的空间量应为OffscreenPageLimit*2个单页面巨细,核算出来的成果会存储在int数组类型的extraLayoutSpace结构中,其间:

  • extraLayoutSpace[0]使用于顶部或左边的额定空间;
  • extraLayoutSpace[1]使用于底部或右侧的额定空间。

尽管这部分额定创立的页面在当时屏幕上并不可见,但实践现已被增加至咱们的视图层次结构中了。这么做可以减少切换分页时花费在视图创立与布局上的时刻,从而进步ViewPager2滑动时的全体流通度

结合前面两篇文章咱们可以看到,从缓存复用机制到预拉取机制再到现在的离屏加载机制,RecyclerView与ViewPager2在进步滑动流通度方面真的是做了十分多的尽力。

差异在于:

  • 缓存复用机制是经过缓存已创立的页面,以供给给新进入屏幕的页面重用来实现的。
  • 预拉取机制是经过运用UI线程空闲的时机,提早创立并缓存下一个待进入屏幕的页面来实现的。
  • 离屏加载机制则是经过扩展额定的布局空间,以提早创立并保存屏幕两边的页面来实现的。

从调用办法流程上讲,离屏加载机制除了常规的onCreateViewHolder、onBindViewHolder办法之外,还会履行一个多onViewAttachedToWindow办法,以将页面提早增加至咱们的视图层次结构中。

尽管咱们一向着重的是“ViewPager2的离屏加载机制”,但其实,离屏加载机制并不是ViewPager2才引入的新特性,作为ViewPager的改善版本,ViewPager2也只是把早已存在于ViewPager中的这个特性照搬过来罢了,二者的首要差异有以下几点:

  • 关于OffscreenPageLimit默许值的设置
  • 关于OffscreenPageLimit赋值条件的约束

OffscreenPageLimit的默许值设置与赋值条件约束

ViewPager一向为人所诟病的一个点就是,其设置的OffscreenPageLimit默许值为1,且不允许外部传入低于1的修正值,即会强制敞开离屏加载机制

    // 默许的离屏加载约束值为1
    private static final int DEFAULT_OFFSCREEN_PAGES = 1;
    public void setOffscreenPageLimit(int limit) {
        // 小于默许值的数会被强制设为默许值
        if (limit < DEFAULT_OFFSCREEN_PAGES) {
            Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "
                    + DEFAULT_OFFSCREEN_PAGES);
            limit = DEFAULT_OFFSCREEN_PAGES;
        }
        if (limit != mOffscreenPageLimit) {
            mOffscreenPageLimit = limit;
            populate();
        }
    }

这也就意味着,在运用ViewPager构建的滑动视图中,不管开发者需不需求,都至少会有1~2个页面会被离屏加载,而这会导致一系列依赖于Fragment生命周期的逻辑被反常履行,进而发生非预期的成果,需求开发者手动实现推迟加载机制。

相比较之下,ViewPager2设置的OffscreenPageLimit默许值则为-1,也即默许不敞开离屏加载机制,且关于外部传入的修正值也只要求有必要是大于0的正数或默许值。

     // 默许的离屏加载约束值为-1
    public static final int OFFSCREEN_PAGE_LIMIT_DEFAULT = -1;
    public void setOffscreenPageLimit(@OffscreenPageLimit int limit) {
        // 低于1且非默许值的传参会报反常
        if (limit < 1 && limit != OFFSCREEN_PAGE_LIMIT_DEFAULT) {
            throw new IllegalArgumentException(
                    "Offscreen page limit must be OFFSCREEN_PAGE_LIMIT_DEFAULT or a number > 0");
        }
        mOffscreenPageLimit = limit;
        // 触发从头布局操作,以便经过getExtraLayoutSize()办法进行离屏加载
        mRecyclerView.requestLayout();
    }

另外,咱们在本系列的第一篇就讲了,ViewPager2是在RecyclerView的基础上构建而成的。因而,即使是默许不敞开离屏加载机制,预拉取机制也会正常作业。

而咱们前面又讲了,预拉取机制会提早创立并缓存下一个待进入屏幕的页面,但不会增加至咱们的视图层次结构中,因而不会像ViewPager相同,导致一系列依赖于Fragment生命周期的逻辑被反常履行,相当于自动帮咱们实现推迟加载机制了。

从以上2个默许数值咱们可以看到,无论是ViewPager仍是ViewPager2,其关于OffscreenPageLimit默许值的设置都是比较抑制的。实践上,在setOffscreenPageLimit办法的注释中,Android也是建议咱们将此约束值坚持在较低水平,尤其是当咱们的页面具有复杂的布局时。

但实践状况是,大部分的开发者为图便利,往往会将此值设为页面总数-1,也即默许会离屏加载一切的页面

这种做法无疑是很不标准的,为什么说不标准呢?这就引申出咱们下一个问题了,即OffscreenPageLimit的不同赋值,会对ViewPager2发生什么样的影响呢?

不同的OffscreenPageLimit值发生的影响

行为表现

OffscreenPageLimit值为-1

当OffscreenPageLimit值为-1时,也即坚持默许不敞开离屏加载机制,这种状况下只要RecyclerView的缓存复用机制和预拉取机制会作业。

【动画图解】这个值取对了,ViewPager2才能纵享丝滑

  1. 当滑动视图初始化完结时,只要position=0的页面项会被增加至当时视图层次结构中。
  2. 跟着咱们往左滑动屏幕,预拉取机制会开端作业,提早创立position=2的页面项并放入mCachedView中。
  3. 一起,position=0的页面项也将跟着向左滑动的手势被移出屏幕,并放入mCachedView中。

【动画图解】这个值取对了,ViewPager2才能纵享丝滑

  1. 再次向左滑动屏幕,滑动视图会取出预拉取的position=2的页面项进行运用,一起敞开对position=3的页面项的预拉取。
  2. 此刻,因为还未超越mCachedView巨细的约束,下一个被移出屏幕的position=1的页面项也将放入mCachedView中。

【动画图解】这个值取对了,ViewPager2才能纵享丝滑

  1. 第三次向左滑动屏幕,相同,会取出预拉取的position=3的页面项进行运用,一起敞开对position=4的页面项的预拉取。
  2. 可是,因为超越了mCachedView巨细的约束,鄙人一个被移出屏幕的position=2的页面项测验进入时,会先依照先进先出的次序,先从mCachedView中移出position=0的页面项,放入RecyclerPool中对应itemType的ArrayList容器中,然后position=2的页面项才顺畅进入mCachedView。
  3. 之后的滑动相同遵从这个规则,不再赘述。
OffscreenPageLimit值为1

当OffscreenPageLimit值为1时,也即会在左右两边各离屏加载1个页面。

【动画图解】这个值取对了,ViewPager2才能纵享丝滑

  1. 当滑动视图初始化完结时,因为左边无更多的页面项,因而只要position=0及position=1的页面项会被增加至当时视图层次结构中。
  2. 跟着咱们往左滑动屏幕,position=2的页面项会被增加至当时视图层次结构中,而position=0的页面项会持续保存在当时视图层次结构中,一起预拉取机制会开端作业,提早创立position=3的页面项并放入mCachedView中。

【动画图解】这个值取对了,ViewPager2才能纵享丝滑

  1. 再次向左滑动屏幕,滑动视图会取出预拉取的position=3的页面项增加至当时视图层次结构中,而position=1的页面项会持续保存在当时视图层次结构中,并敞开对position=4的页面项的预拉取。
  2. 一起,position=0的页面项也将跟着向左滑动的手势被移出屏幕,并放入mCachedView中。

【动画图解】这个值取对了,ViewPager2才能纵享丝滑

  1. 第三次向左滑动屏幕,相同,会取出预拉取的position=4的页面项增加至当时视图层次结构中,并保存position=2的页面项在当时视图层次结构中,一起敞开对position=5的页面项的预拉取。
  2. 此刻,因为还未超越mCachedView巨细的约束,下一个被移出屏幕的position=1的页面项也将放入mCachedView中。

【动画图解】这个值取对了,ViewPager2才能纵享丝滑

  1. 第四次向左滑动屏幕,相同,会取出预拉取的position=5的页面项增加至当时视图层次结构中,并保存position=3的页面项在当时视图层次结构中,一起敞开对position=6的页面项的预拉取。
  2. 可是,因为超越了mCachedView巨细的约束,鄙人一个被移出屏幕的position=2的页面项测验进入时,会先依照先进先出的次序,先从mCachedView中移出position=0的页面项,放入RecyclerPool中对应itemType的ArrayList容器中。
OffscreenPageLimit值为3

当OffscreenPageLimit值为3时,也即会在左右两边各离屏加载3个页面。

【动画图解】这个值取对了,ViewPager2才能纵享丝滑

  1. 当滑动视图初始化完结时,因为左边无更多的页面项,因而只要position=0至position=3的页面项会被增加至当时视图层次结构中。
  2. 跟着咱们往左滑动屏幕,position=4的页面项会被增加至当时视图层次结构中,而position=0的页面项会持续保存在当时视图层次结构中,一起预拉取机制会开端作业,提早创立position=5的页面项并放入mCachedView中。

【动画图解】这个值取对了,ViewPager2才能纵享丝滑

  1. 再次向左滑动屏幕,滑动视图会取出预拉取的position=5的页面项增加至当时视图层次结构中,而position=1的页面项会持续保存在当时视图层次结构中,并敞开对position=6的页面项的预拉取。

【动画图解】这个值取对了,ViewPager2才能纵享丝滑

  1. 第三次向左滑动屏幕,滑动视图会取出预拉取的position=6的页面项增加至当时视图层次结构中,而position=2的页面项会持续保存在当时视图层次结构中。也即这个时分,一切的页面项现已都被增加至当时视图层次结构中了。

【动画图解】这个值取对了,ViewPager2才能纵享丝滑

  1. 第四次向左滑动屏幕,因为超出了OffscreenPageLimit值,position=0的页面项将跟着向左滑动的手势被移出屏幕,并放入mCachedView中。

【动画图解】这个值取对了,ViewPager2才能纵享丝滑

  1. 第五次向左滑动屏幕,此刻,因为还未超越mCachedView巨细的约束,下一个被移出屏幕的position=1的页面项也将放入mCachedView中。

【动画图解】这个值取对了,ViewPager2才能纵享丝滑

  1. 第六次向左滑动屏幕,可是,因为超越了mCachedView巨细的约束,鄙人一个被移出屏幕的position=2的页面项测验进入时,会先依照先进先出的次序,先从mCachedView中移出position=0的页面项,放入RecyclerPool中对应itemType的ArrayList容器中。
OffscreenPageLimit值为页面总数-1

当OffscreenPageLimit值为页面总数-1时,也即在滑动视图初始化完结时就现已离屏加载一切的页面了,这种状况下RecyclerView的缓存复用机制和预拉取机制完全没有作业的时机。

尽管设置更高的OffscreenPageLimit值,可以更好地进步ViewPager2滑动时的流通度,但因为需求在初始化阶段一起创立多个页面项,意味着将花费更久的创立时刻,页面项内容也将更慢显示,一起,因为两边有更多的页面项被保存而不走缓存复用流程,意味着使用会占用更多的内存,且这些问题将跟着页面复杂度进步愈加杰出。

为了更直观地展现不同的OffscreenPageLimit值对使用的性能影响,咱们将从白屏时刻、流通度、占用内存三个维度来进行横向对比:

性能影响

白屏时刻

【动画图解】这个值取对了,ViewPager2才能纵享丝滑

可以看到,跟着OffscreenPageLimit值的增加,在滑动视图的初始化阶段,会有更多的页面项需求被创立并被增加至当时的视图层次结构中,白屏时刻也随之延长。

流通度

参考上一篇的做法,咱们相同在FragmentStateAdapter中对Fragment的视图准备作业做了推迟,以在GPU渲染形式中展现愈加明晰的柱状图:

【动画图解】这个值取对了,ViewPager2才能纵享丝滑

OffscreenPageLimit值为1时,尽管可以离屏加载下一个页面,但因为每次滑动还要履行预拉取的作业,因而关于流通度的进步不是很明显。

OffscreenPageLimit值为3时,即每次都会保存当时屏幕两边的各3个页面项,在滑动到中心方位时,关于流通度的进步是最大的,此刻无论是往前滑仍是往后滑,都无需再履行页面项的创立作业,即使滑到边界也可以运用缓存复用机制来重用视图。

OffscreenPageLimit值为6时,也即在滑动视图初始化完结时就现已离屏加载一切的页面了,每次的滑动就相当于只是在当时的视图层次结构中进行位移,因而全程的流通度都有极大的进步。

内存占用

可以看到,跟着OffscreenPageLimit值的增加,在滑动视图的初始化阶段,会有更多的Fragment目标驻留在内存中。

【动画图解】这个值取对了,ViewPager2才能纵享丝滑

一起,因为OffscreenPageLimit值会保存当时屏幕两边的页面项,因而,滑动到中心方位时,OffscreenPageLimit值为1的状况最多会保存3个Fragment目标,而OffscreenPageLimit值为3的状况最多会保存7个Fragment目标。

但在其他方位时,它会将超出OffscreenPageLimit值约束的页面将从视图层次结构中移除,并交由RecyclerView的缓存复用机制处理,同往常相同回收ViewHolders目标以供重用。

OffscreenPageLimit值取多大比较适宜?

现在咱们知道了,当OffscreenPageLimit值设得过大,比如页面总数-1时,会给使用带来比较大的内存压力,特别是在部分低端机型上。

而OffscreenPageLimit值设得过小,比如1时,又无法发挥出离屏加载机制进步页面滑动流通度的优势。

一般来讲,一起坚持3-4个页面项处于活动状况是一个比较适宜的值,一方面,可以进步用户来回翻页时的流通度,另一方面又不会给使用带来太大的内存压力。当然,还需求咱们自己维护好Fragment重建以及视图回收/复用时的处理逻辑。

最好的状况下,仍是希望可以根据使用当时的内存运用状况,对该值进行动态调整,在行为表现与性能影响上取一个平衡点。

但如果多个页面项之间存在互斥关系,一起处于活动状况可能影响业务的判别时,坚持OffscreenPageLimit为默许值,也即默许关闭离屏加载机制,只让预拉取机制与缓存复用机制作业,也许是个更好的选择。

后记

讲到这里,相信你对ViewPager2的离屏加载机制现已有了必定的认识,但不知道你发现没有,咱们全文讲的都是ViewPager2次序依次翻页的状况,但在实践运用中,咱们常常会搭配TabLayout,供给点击标签页跳转到指定页面项的功用。

而当增加了这一种新的交互办法后,问题的维度再一次上升了,咱们会发现离屏加载机制的行为逻辑又有所不同了,而这,就是下一篇的内容了。