------《ViewPage2离屏加载》
- 前言
- 离屏加载是什么
- OffscreenPageLimit
- 设置OffscreenPageLimit表现
- OffscreenPageLimit值为1
- OffscreenPageLimit值为3
- OffscreenPageLimit值取多大比较合适
前言
这里就不讲ViewPage了,买新不买旧,用新不用旧。
但是会将ViewPage和ViewPage2进行对比一下。
ViewPage2是基于RecyclerView进行处理的。所以他也继承了RecyclerView的缓存和预加载机制。
为了看清离屏加载的机制,可以先关闭一下预加载。
((RecyclerView)viewPager.getChildAt(0)).getLayoutManager().setItemPrefetchEnabled(false);
离屏加载是什么
离屏加载最主要的控制参数: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方法,以将页面提前添加至我们的视图层次结构中。
OffscreenPageLimit
ViewPager一直为人所诟病的一个点就是,其设置的OffscreenPageLimit默认值为1,且不允许外部传入低于1的修改值,即会「强制开启离屏加载机制」。
这也就意味着,在使用ViewPager构建的滑动视图中,不管开发者需不需要,都至少会有1~2个页面会被离屏加载,而这会导致一系列依赖于Fragment生命周期的逻辑被异常执行,进而产生非预期的结果,需要开发者手动实现延迟加载机制。
相比较之下,ViewPager2设置的OffscreenPageLimit默认值则为-1,也即「默认不开启离屏加载机制」
这种情况下只有RecyclerView的缓存复用机制和预拉取机制会工作。
public static final int OFFSCREEN_PAGE_LIMIT_DEFAULT = -1;
且对于外部传入的修改值可以为大于1的整数。
public void setOffscreenPageLimit(@OffscreenPageLimit int limit) {
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;
// Trigger layout so prefetch happens through getExtraLayoutSize()
mRecyclerView.requestLayout();
}
另外,ViewPager2是在RecyclerView的基础上构建而成的。因此,即使是默认不开启离屏加载机制,预拉取机制也会正常工作。
但实际情况是,大部分的开发者为图方便,往往会将此值设为「页面总数-1」,也就是缓存所有的页面。但是这是不规范的。
设置OffscreenPageLimit表现
OffscreenPageLimit值为1
当OffscreenPageLimit值为1时,也即会在左右两侧各离屏加载1个页面。
1、当滑动视图初始化完成时,由于左侧无更多的页面项,因此只有position=0及position=1的页面项会被添加至当前视图层次结构中。
2、随着我们往左滑动屏幕,position=2的页面项会被添加至当前视图层次结构中,而position=0的页面项会继续保留在当前视图层次结构中,同时预拉取机制会开始工作,提前创建position=3的页面项并放入mCachedView中。
3、再次向左滑动屏幕,滑动视图会取出预拉取的position=3的页面项添加至当前视图层次结构中,而position=1的页面项会继续保留在当前视图层次结构中,并开启对position=4的页面项的预拉取。
4、同时,position=0的页面项也将随着向左滑动的手势被移出屏幕,并放入mCachedView中
5、第三次向左滑动屏幕,同样,会取出预拉取的position=4的页面项添加至当前视图层次结构中,并保留position=2的页面项在当前视图层次结构中,同时开启对position=5的页面项的预拉取。
6、此时,由于还未超过mCachedView大小的限制,下一个被移出屏幕的position=1的页面项也将放入mCachedView中。
7、第四次向左滑动屏幕,同样,会取出预拉取的position=5的页面项添加至当前视图层次结构中,并保留position=3的页面项在当前视图层次结构中,同时开启对position=6的页面项的预拉取。
8、但是,由于超过了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中。
3、再次向左滑动屏幕,滑动视图会取出预拉取的position=5的页面项添加至当前视图层次结构中,而position=1的页面项会继续保留在当前视图层次结构中,并开启对position=6的页面项的预拉取。
缓存全部的页面的话就设置OffscreenPageLimit为页面的总数 - 1即可。
OffscreenPageLimit值取多大比较合适
当OffscreenPageLimit值设得过大,会给应用带来比较大的内存压力,特别是在部分低端机型上。
而OffscreenPageLimit值设得过小,比如1时,又无法发挥出离屏加载机制提高页面滑动流畅度的优势。
一般来讲,同时保持3-4个页面项处于活动状态是一个比较合适的值,一方面,可以提高用户来回翻页时的流畅度,另一方面又不会给应用带来太大的内存压力。当然,还需要我们自己维护好Fragment重建以及视图回收/复用时的处理逻辑。
最好的情况下,还是希望能够根据应用当前的内存使用情况,对该值进行动态调整,在行为表现与性能影响上取一个平衡点。