绍兴市科信建设工程检测中心网站,胶南网站建设哪家好,中小学网站建设排行2017,互联网设计师leader前言
在前两篇文章中#xff0c;我们通过一张张清晰明了的「示意图」#xff0c;详细地复盘了RecyclerView「缓存复用机制」与「预拉取机制」的工作流程#xff0c;这种「图解」创作形式也得到了来自不同平台读者们的一致认可。
而从本文开始#xff0c;我们将正式进入Vi…前言
在前两篇文章中我们通过一张张清晰明了的「示意图」详细地复盘了RecyclerView「缓存复用机制」与「预拉取机制」的工作流程这种「图解」创作形式也得到了来自不同平台读者们的一致认可。
而从本文开始我们将正式进入ViewPager2的篇章并将辅以更加生动易懂的「动态示意图」来进行讲解。
ViewPager2可讲的内容有很多今天我们主要介绍是ViewPager2的「离屏加载机制」你可能是第一次听说这个术语但在实际开发中你肯定使用过它因为它对应的配置入口就是ViewPager2的OffscreenPageLimit属性。
OffscreenPageLimit是什么
OffscreenPageLimit直译过来是离屏页面限制值的意思该值代表的是在滑动视图中应保留在当前可见页面之外的任一方向上的页面数。
比如当我们采用水平分页时该值代表的便是在左右两侧应保留的页面数。 而当我们采用垂直分页时该值代表的则是在上下两侧应保留的页面数。 保留页面的方式是通过扩展额外的布局空间实现的以LinearLayoutManager为例其最关键的步骤在于对calculateExtraLayoutSpace方法的重写 /*** 计算额外的布局空间*/Overrideprotected 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的修改值即会强制开启离屏加载机制。 // 默认的离屏加载限制值为1private 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的正数或默认值。 // 默认的离屏加载限制值为-1public 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的缓存复用机制和预拉取机制会工作。 当滑动视图初始化完成时只有position0的页面项会被添加至当前视图层次结构中。随着我们往左滑动屏幕预拉取机制会开始工作提前创建position2的页面项并放入mCachedView中。同时position0的页面项也将随着向左滑动的手势被移出屏幕并放入mCachedView中。 再次向左滑动屏幕滑动视图会取出预拉取的position2的页面项进行使用同时开启对position3的页面项的预拉取。此时由于还未超过mCachedView大小的限制下一个被移出屏幕的position1的页面项也将放入mCachedView中。 第三次向左滑动屏幕同样会取出预拉取的position3的页面项进行使用同时开启对position4的页面项的预拉取。但是由于超过了mCachedView大小的限制在下一个被移出屏幕的position2的页面项尝试进入时会先按照先进先出的顺序先从mCachedView中移出position0的页面项放入RecyclerPool中对应itemType的ArrayList容器中然后position2的页面项才顺利进入mCachedView。之后的滑动同样遵循这个规律不再赘述。
OffscreenPageLimit值为1
当OffscreenPageLimit值为1时也即会在左右两侧各离屏加载1个页面。 当滑动视图初始化完成时由于左侧无更多的页面项因此只有position0及position1的页面项会被添加至当前视图层次结构中。随着我们往左滑动屏幕position2的页面项会被添加至当前视图层次结构中而position0的页面项会继续保留在当前视图层次结构中同时预拉取机制会开始工作提前创建position3的页面项并放入mCachedView中。 再次向左滑动屏幕滑动视图会取出预拉取的position3的页面项添加至当前视图层次结构中而position1的页面项会继续保留在当前视图层次结构中并开启对position4的页面项的预拉取。同时position0的页面项也将随着向左滑动的手势被移出屏幕并放入mCachedView中。 第三次向左滑动屏幕同样会取出预拉取的position4的页面项添加至当前视图层次结构中并保留position2的页面项在当前视图层次结构中同时开启对position5的页面项的预拉取。此时由于还未超过mCachedView大小的限制下一个被移出屏幕的position1的页面项也将放入mCachedView中。 第四次向左滑动屏幕同样会取出预拉取的position5的页面项添加至当前视图层次结构中并保留position3的页面项在当前视图层次结构中同时开启对position6的页面项的预拉取。但是由于超过了mCachedView大小的限制在下一个被移出屏幕的position2的页面项尝试进入时会先按照先进先出的顺序先从mCachedView中移出position0的页面项放入RecyclerPool中对应itemType的ArrayList容器中。
OffscreenPageLimit值为3
当OffscreenPageLimit值为3时也即会在左右两侧各离屏加载3个页面。 当滑动视图初始化完成时由于左侧无更多的页面项因此只有position0至position3的页面项会被添加至当前视图层次结构中。随着我们往左滑动屏幕position4的页面项会被添加至当前视图层次结构中而position0的页面项会继续保留在当前视图层次结构中同时预拉取机制会开始工作提前创建position5的页面项并放入mCachedView中。 再次向左滑动屏幕滑动视图会取出预拉取的position5的页面项添加至当前视图层次结构中而position1的页面项会继续保留在当前视图层次结构中并开启对position6的页面项的预拉取。 第三次向左滑动屏幕滑动视图会取出预拉取的position6的页面项添加至当前视图层次结构中而position2的页面项会继续保留在当前视图层次结构中。也即这个时候所有的页面项已经都被添加至当前视图层次结构中了。 第四次向左滑动屏幕由于超出了OffscreenPageLimit值position0的页面项将随着向左滑动的手势被移出屏幕并放入mCachedView中。 第五次向左滑动屏幕此时由于还未超过mCachedView大小的限制下一个被移出屏幕的position1的页面项也将放入mCachedView中。 第六次向左滑动屏幕但是由于超过了mCachedView大小的限制在下一个被移出屏幕的position2的页面项尝试进入时会先按照先进先出的顺序先从mCachedView中移出position0的页面项放入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提供点击标签页跳转到指定页面项的功能。
而当增加了这一种新的交互方式后问题的维度再一次上升了我们会发现离屏加载机制的行为逻辑又有所不同了而这就是下一篇的内容了。