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

news2024/10/2 20:38:31

前言

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

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

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

OffscreenPageLimit是什么?

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

比如,当我们采用水平分页时,该值代表的便是在左右两侧应保留的页面数。

而当我们采用垂直分页时,该值代表的则是在上下两侧应保留的页面数。

保留页面的方式是通过扩展额外的布局空间实现的,以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的缓存复用机制和预拉取机制会工作。

  1. 当滑动视图初始化完成时,只有position=0的页面项会被添加至当前视图层次结构中。
  2. 随着我们往左滑动屏幕,预拉取机制会开始工作,提前创建position=2的页面项并放入mCachedView中。
  3. 同时,position=0的页面项也将随着向左滑动的手势被移出屏幕,并放入mCachedView中。

  1. 再次向左滑动屏幕,滑动视图会取出预拉取的position=2的页面项进行使用,同时开启对position=3的页面项的预拉取。
  2. 此时,由于还未超过mCachedView大小的限制,下一个被移出屏幕的position=1的页面项也将放入mCachedView中。

  1. 第三次向左滑动屏幕,同样,会取出预拉取的position=3的页面项进行使用,同时开启对position=4的页面项的预拉取。
  2. 但是,由于超过了mCachedView大小的限制,在下一个被移出屏幕的position=2的页面项尝试进入时,会先按照先进先出的顺序,先从mCachedView中移出position=0的页面项,放入RecyclerPool中对应itemType的ArrayList容器中,然后position=2的页面项才顺利进入mCachedView。
  3. 之后的滑动同样遵循这个规律,不再赘述。
OffscreenPageLimit值为1

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

  1. 当滑动视图初始化完成时,由于左侧无更多的页面项,因此只有position=0及position=1的页面项会被添加至当前视图层次结构中。
  2. 随着我们往左滑动屏幕,position=2的页面项会被添加至当前视图层次结构中,而position=0的页面项会继续保留在当前视图层次结构中,同时预拉取机制会开始工作,提前创建position=3的页面项并放入mCachedView中。

  1. 再次向左滑动屏幕,滑动视图会取出预拉取的position=3的页面项添加至当前视图层次结构中,而position=1的页面项会继续保留在当前视图层次结构中,并开启对position=4的页面项的预拉取。
  2. 同时,position=0的页面项也将随着向左滑动的手势被移出屏幕,并放入mCachedView中。

  1. 第三次向左滑动屏幕,同样,会取出预拉取的position=4的页面项添加至当前视图层次结构中,并保留position=2的页面项在当前视图层次结构中,同时开启对position=5的页面项的预拉取。
  2. 此时,由于还未超过mCachedView大小的限制,下一个被移出屏幕的position=1的页面项也将放入mCachedView中。

  1. 第四次向左滑动屏幕,同样,会取出预拉取的position=5的页面项添加至当前视图层次结构中,并保留position=3的页面项在当前视图层次结构中,同时开启对position=6的页面项的预拉取。
  2. 但是,由于超过了mCachedView大小的限制,在下一个被移出屏幕的position=2的页面项尝试进入时,会先按照先进先出的顺序,先从mCachedView中移出position=0的页面项,放入RecyclerPool中对应itemType的ArrayList容器中。
OffscreenPageLimit值为3

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

  1. 当滑动视图初始化完成时,由于左侧无更多的页面项,因此只有position=0至position=3的页面项会被添加至当前视图层次结构中。
  2. 随着我们往左滑动屏幕,position=4的页面项会被添加至当前视图层次结构中,而position=0的页面项会继续保留在当前视图层次结构中,同时预拉取机制会开始工作,提前创建position=5的页面项并放入mCachedView中。

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

  1. 第三次向左滑动屏幕,滑动视图会取出预拉取的position=6的页面项添加至当前视图层次结构中,而position=2的页面项会继续保留在当前视图层次结构中。也即这个时候,所有的页面项已经都被添加至当前视图层次结构中了。

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

  1. 第五次向左滑动屏幕,此时,由于还未超过mCachedView大小的限制,下一个被移出屏幕的position=1的页面项也将放入mCachedView中。

  1. 第六次向左滑动屏幕,但是,由于超过了mCachedView大小的限制,在下一个被移出屏幕的position=2的页面项尝试进入时,会先按照先进先出的顺序,先从mCachedView中移出position=0的页面项,放入RecyclerPool中对应itemType的ArrayList容器中。
OffscreenPageLimit值为页面总数-1

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

虽然设置更高的OffscreenPageLimit值,可以更好地提升ViewPager2滑动时的流畅度,但由于需要在初始化阶段同时创建多个页面项,意味着将花费更久的创建时间,页面项内容也将更慢显示,同时,由于两侧有更多的页面项被保留而不走缓存复用流程,意味着应用会占用更多的内存,且这些问题将随着页面复杂度提升更加突出。

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

性能影响

白屏时间

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

流畅度

参考上一篇的做法,我们同样在FragmentStateAdapter中对Fragment的视图准备工作做了延迟,以在GPU渲染模式中展示更加清晰的柱状图:

OffscreenPageLimit值为1时,虽然可以离屏加载下一个页面,但由于每次滑动还要执行预拉取的工作,因此对于流畅度的提升不是很明显。

OffscreenPageLimit值为3时,即每次都会保留当前屏幕两侧的各3个页面项,在滑动到中间位置时,对于流畅度的提升是最大的,此时无论是往前滑还是往后滑,都无需再执行页面项的创建工作,即使滑到边界也可以利用缓存复用机制来重用视图。

OffscreenPageLimit值为6时,也即在滑动视图初始化完成时就已经离屏加载所有的页面了,每次的滑动就相当于只是在当前的视图层次结构中进行位移,因此全程的流畅度都有极大的提升。

内存占用

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

同时,由于OffscreenPageLimit值会保留当前屏幕两侧的页面项,因此,滑动到中间位置时,OffscreenPageLimit值为1的情况最多会保留3个Fragment对象,而OffscreenPageLimit值为3的情况最多会保留7个Fragment对象。

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

OffscreenPageLimit值取多大比较合适?

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

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

一般来讲,同时保持3-4个页面项处于活动状态是一个比较合适的值,一方面,可以提高用户来回翻页时的流畅度,另一方面又不会给应用带来太大的内存压力。当然,还需要我们自己维护好Fragment重建以及视图回收/复用时的处理逻辑。

最好的情况下,还是希望能够根据应用当前的内存使用情况,对该值进行动态调整,在行为表现与性能影响上取一个平衡点。

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

后记

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/347875.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

采用Spring配置文件管理Bean(1)

文章目录一、创建Maven项目二、创建杀龙任务类三、创建勇敢骑士类四、采用传统方式让勇敢骑士完成杀龙任务五、采用Spring容器让勇敢骑士完成杀龙任务1、创建日志属性文件2、创建Spring配置文件3、在Spring配置文件里创建Bean&#xff08;1&#xff09;创建杀龙任务Bean&#x…

VAmPI:一个包含了OWASP Top10漏洞的REST API安全学习平台

关于VAmPI VAmPI是一个包含了OWASP Top10漏洞的REST API安全学习平台&#xff0c;该平台基于Flask开发&#xff0c;该工具的主要目的是通过一个易受攻击的API来评估针对API安全检测工具的有效性&#xff0c;并帮助广大研究人员学习和了解API安全。 功能介绍 1、基于OWASP Top…

Spring入门学习

Spring入门学习 文章目录Spring入门学习Spring概述Spring FrameworkIOCIOC容器DIIOC容器的实现类①FileSystemXmlApplicationContext②ClassPathXmlApplicationContext基于XML管理bean入门案例创建类创建xml在Spring配置文件中配置bean测试Spring概述 Spring 是最受欢迎的企业级…

利用飞书来实现信息存储和在线远程预览

利用飞书来实现信息存储和在线远程预览 文章目录利用飞书来实现信息存储和在线远程预览1. 需求2. 实现方法2.1 创建表格2.2 创建飞书捷径2.3 客户端上传数据3. 实现远程预览1. 需求 在传统的远程监控和存储数据的项目中&#xff0c;我们需要一台有公网IP的服务器来存储设备发上…

75V的TVS二极管有哪些型号?常用的

瞬态抑制TVS二极管工作峰值反向电压最低3.3V&#xff0c;最高可达513V&#xff0c;甚至更高。很多电子工程师都知道&#xff0c;TVS二极管在实际应用选型过程中&#xff0c;第一步要确认的就是其工作峰值反向电压。2023年春节已过&#xff0c;东沃电子正月初八就开工了&#xf…

【JavaEE】多线程代码实例:单例模式与阻塞队列BlockingQueue

目录 单例模式&#xff1a; 什么是单例模式&#xff1f; 单例模式的实现方式&#xff1a; 饿汉模式&#xff1a; 懒汉模式&#xff1a; 基于并发编程对单例模式线程安全问题的讨论&#xff1a; 阻塞队列&#xff1a; 标准库中的阻塞队列&#xff1a; 自实现阻塞…

CPP2022-计算机类-期末考试

6-1 判断素数 分数 5 全屏浏览题目 切换布局 作者 李国瑞 单位 东北大学秦皇岛分校 设计一个函数&#xff0c;判断输入数据是否为素数&#xff0c;返回bool类型结果。 函数接口定义&#xff1a; bool prime(int num); 说明&#xff1a;num为正整数。 裁判测试程序样例&…

基于node.js+vue+mysql考研辅导学习打卡交流网站系统vscode

语言 node.js 框架&#xff1a;Express 前端:Vue.js 数据库&#xff1a;mysql 数据库工具&#xff1a;Navicat 开发软件&#xff1a;VScode 主要功能包括管理员&#xff1a;首页、个人中心、用户管理、每日打卡管理、考研学校管理、考研专业管理、直通车管理、学习教材管理、…

云平台搭建实例

嗨嗨&#xff0c;每天一更是不是很奈斯&#xff1f;我也觉得&#xff0c;昨天晚上我学校的老师借一天一千的设备&#xff0c;只能用七天&#xff0c;所以我拿出来给你们没有设备和刚用设备的看看吧。操作&#xff1a;首先我们将云平台安装好后&#xff0c;插上网线&#xff0c;…

手机截图如何提取文字?

在当今信息爆炸的时代&#xff0c;图文并茂已经成为了一个广告宣传的常用方式。然而&#xff0c;图片中的文字信息往往难以获取&#xff0c;尤其对于那些需要快速获取信息的人们来说&#xff0c;阅读图片中的文字会是一项繁琐且费时的任务。现在&#xff0c;我们有一个好消息要…

C语言的期末复习

&#x1f308;博客主页&#xff1a;卿云阁 &#x1f48c;欢迎关注&#x1f389;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f31f;本文由卿云阁原创&#xff01; &#x1f64f;作者水平很有限&#xff0c;如果发现错误&#xff0c;请留言轰炸哦&#xff01;万分感谢&a…

数据结构:链表基础OJ练习+带头双向循环链表的实现

目录 一.leetcode剑指 Offer II 027. 回文链表 1.问题描述 2.问题分析与求解 (1) 快慢指针法定位链表的中间节点 (2) 将链表后半部分进行反转 附:递归法反转链表 (3) 双指针法判断链表是否回文 二.带头双向循环链表的实现 1.头文件 2.节点内存申请接口和链表初始化接口…

virtuoso数据库介绍

在国内&#xff0c;对海量 RDF 数据的管理有着迫切的实际需求&#xff1b; RDF&#xff1a;Resource Description Framework&#xff0c;是一个使用XML语法来表示的资料模型(Data model)&#xff0c;用来描述Web资源的特性&#xff0c;及资源与资源之间的关系。 Virtuoso可以对…

今天正式上线!虹科汽车免拆诊断云展厅:感受精准修车魅力,畅享汽修领先技术

『虹科汽车免拆诊断云展厅』 2月15日正式上线&#xff01; 在这里&#xff0c;您可以参观了解&#xff1a; 虹科Pico汽车示波器产品模型 全流程专业讲解的视频资料 产品功能和应用场景 全面详细的产品手册 还有虹科首席技术工程师在线连麦答疑&#xff01;&#xff01;&#xf…

硬核!2023最全Java面试八股文,覆盖市面上80%以上的面试考点

就目前大环境来看&#xff0c;跳槽成功的难度比往年高很多。一个明显的感受&#xff1a;今年的面试&#xff0c;无论一面还是二面&#xff0c;都很考验 Java 程序员的技术功底。这不马上又到了面试跳槽的黄金段&#xff0c;成功升职加薪&#xff0c;不成功饱受打击。当然也要注…

影像测量设备都有什么?有哪些影像仪器?

影像测量仪器是广泛应用于机械、电子、仪表的仪器。主要由机械主体、标尺系统、影像探测系统、驱动控制系统和测量软件等与高精密工作台结构组成的光电测量仪器。一般分为三大类&#xff1a;手动影像仪、自动影像仪和闪测影像仪。测量元素主要有&#xff1a;长度、宽度、高度、…

【C语言技能树】浮点数在内存中的存储

Halo&#xff0c;这里是Ppeua。平时主要更新C语言&#xff0c;C&#xff0c;数据结构算法......感兴趣就关注我吧&#xff01;你定不会失望。 &#x1f308;个人主页&#xff1a;主页链接 &#x1f308;算法专栏&#xff1a;专栏链接 我会一直往里填充内容哒&#xff01; &…

CSDN每日一练:小豚鼠搬家

题目名称&#xff1a;小豚鼠搬家 时间限制&#xff1a;1000ms内存限制&#xff1a;256M 题目描述 小豚鼠排排坐。 小艺酱买了一排排格子的小房子n*m&#xff0c;她想让k只小豚鼠每只小豚鼠都有自己的房子。 但是为了不浪费空间&#xff0c;她想要小房子的最外圈尽量每行每列都有…

TCP/IP网络编程——多种 I/O 函数

完整版文章请参考&#xff1a; TCP/IP网络编程完整版文章 文章目录第 13 章 多种 I/O 函数13.1 send & recv 函数13.1.1 Linux 中的 send & recv13.1.2 MSG_OOB&#xff1a;发送紧急消息13.1.3 紧急模式工作原理13.1.4 检查输入缓冲13.2 readv & writev 函数13.2.1…

深入探讨软件定义架构及其意义

在上期文章中&#xff0c;我们了解了现代GNSS模拟中的软件定义架构&#xff0c;并与传统架构进行了对比&#xff0c;本期文章中我们将继续深入探讨软件定义架构及其意义。所谓软件定义架构&#xff0c;其实是用软件去定义系统的功能&#xff0c;用软件给硬件赋能&#xff0c;最…