引言
本文不再介绍ViewPager1 or ViewPager2的使用方式,而是直接描述其原理,介绍其预加载、缓存、懒加载等相关。给出相关示例,最后给出多层Fragment懒加载的最终代码。
原理
缓存和预加载
ViewPager至少会缓存两针数据,尽管你通过setOffscreenPageLimit(0)来希望不缓存任何数据,但发现起不到任何作用,从ViewPager1的源码中我们可以发现:
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原有的特性和能力。
值得注意的是,在函数中,我们发现它调用了populate()方法,该方法在onMeasure时也会被调用,它用来预加载和缓存ViewPager的Fragment。
当前Current的左右会各设置一个缓存项,同时由于ViewPager预加载的存在,导致缓存页会也走到onCreateView等生命周期,若你刚好是在onCreateView的时候加载页面,那么就会白白浪费两个页面的内存。
因此,我们希望只在显示页面的时候加载页面,此时就需要用到懒加载。
ViewPager1懒加载
ViewPager + Fragment(注意,这个Fragment不在androidx包),由于预加载的存在,一开始,ViewPager就会将currentItem,以及left cache item和right cache item都加载,就会执行Fragment的生命周期,就会直接来加载页面。
通过抽象源码来看主要预加载流程:
public void populate(mCurItem) {
// ...
mAdapter.startUpdate(this);
// ...
curItem = addNewItem(mCurItem, curIndex);
// ...
for (pos = mCurItem-1; pos>=0; pos--) {
if (不在预加载范围) {
mItems.remove(itemIndex);
mAdapter.destroyItem(this, pos, ii.object);
// ...
} else if (在范围,但被加载过) {
// 忽略,只进行计数等...
} else { // 在范围,没有被加载过
addNewItem(pos, itemIndex + 1);
// ...
}
}
// 同上
for (post = mCurItem+1; post<N;post ++) {
if (不在预加载范围) {
mItems.remove(itemIndex);
mAdapter.destroyItem(this, pos,...);
// ...
} else if (在范围,但被加载过) {
// 忽略,只进行计数等...
} else { // 在范围,没有被加载过
addNewItem(pos, itemIndex + 1);
// ...
}
}
// ...
mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
// ...
mAdapter.finishUpdate(this);
}
主要流程:当onMeasure当前ViewPager的时候会调用populate,在populate中执行一系列缓存和预加载。
1、调用mAdapter.startUpDate(this),表示开始加载
2、调用addNewItem->mApdater.instantiateItem()创建Item(Fragment),同时获取了FragmentManager,调用了beginTransaction()
3、循环遍历当前Item左边的item,如果不在预加载范围内,就调用mAdapter.destroyItem()销毁Item,否则如果在预加载范围内,如果已经被加载,就忽略,否则就调用addNewItem加载新的Item
4、循环遍历当前item右边的item,同上
5、调用setPrimaryItem,调用离开的fragment.setUserVisibleHint(false),调用当前的fragment,setUservisibleHint(true)
6、调用mAdapter.finishUpdate,即调用了transaction.commitNowAllowingStateLoss();
从上述流程不难发现,Fragment生命周期在最后finishiUpdate时通过transaction才开始执行,因此,setUserVisibleHint()函数的调用在Fragment生命周期执行之前。
老方案:ViewPager1+Fragment懒加载
值得注意的是,这里的Fragment,我们使用的是android.support.v4.app.Fragment,值得注意的是,在高版本的SDK中,该类已被弃用。
具体ViewPager1+Fragment的使用在本文中不做赘述。接下来,本文将一步步推理出懒加载的实现。
测试1:由于最终需要通过setUserVisibleHint()来设置当前Fragment的可见状态,因此只需在该函数状态改变时,调用onFragmentLoad();或者onFragmentLoadStop();
package com.hc.viewPager_fragment;
import android.app.Fragment;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.Nullable;
public class LazyFragment01 extends Fragment {
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
onFragmentLoad();
} else {
onFragmentLoadStop();
}
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
@Override
public void onResume() {
super.onResume();
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onDestroyView() {
super.onDestroyView();
}
protected void onFragmentLoad() {}
protected void onFragmentLoadStop() {}
}
上述方案的缺陷是,无法在onFragmentLoad和onFragmentLoadStop中获取UI,因为setUserVisibleHint()函数在Fragment生命周期之前调用,否则会出现奔溃。
测试2:在测试1的基础上,让load和loadStop在onCreateView之后调用。
package com.hc.viewPager_fragment;
import android.app.Fragment;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.Nullable;
public class LazyFragment02 extends Fragment {
private boolean isCreated;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (!isCreated) {
return;
}
dispatchVisibleState(isVisibleToUser);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
isCreated = true;
if (getUserVisibleHint()) {
dispatchVisibleState(true);
}
}
private void dispatchVisibleState(boolean isVisibleToUser) {
if (isVisibleToUser) {
onFragmentLoad();
} else {
onFragmentLoadStop();
}
}
@Override
public void onResume() {
super.onResume();
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onDestroyView() {
super.onDestroyView();
isCreated = false;
}
protected void onFragmentLoad() {}
protected void onFragmentLoadStop() {}
}
我们通过变量,isCreated来决定是否分发,并在onCreateView的时候补充一个load或loadStop分发,因为在setUserVisibleHint的时候会过滤调View还没创建的部分。
但又引发了一个从未显示过的页面也将停止加载的问题。通过该方式,的确可以实现对界面懒加载,但不应该未展示过的界面出现停止加载,导致性能损耗。
出现该问题的原因如下:
从上图可知,由1跳到4,则3、4、5加载,最终显示4,其中3、5也将被ViewPager调用setUserVisibleHint(false),使得也会调用onFragmentLoadStop(),导致一些错误的操作。
因此,测试3:我们还需要一个额外的变量来控制。
package com.hc.viewPager_fragment;
import android.app.Fragment;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.Nullable;
public class LazyFragment03 extends Fragment {
private boolean isCreated;
private boolean isPreVisible = false; // 之前是否可见
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (!isCreated) {
return;
}
if (!isPreVisible && isVisibleToUser) {
dispatchVisibleState(true);
} else if(isPreVisible && !isVisibleToUser){
dispatchVisibleState(false);
}
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
isCreated = true;
if (getUserVisibleHint()) {
dispatchVisibleState(true);
}
}
private void dispatchVisibleState(boolean isVisibleToUser) {
isPreVisible = isVisibleToUser;
if (isVisibleToUser) {
onFragmentLoad();
} else {
onFragmentLoadStop();
}
}
@Override
public void onResume() {
super.onResume();
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onDestroyView() {
super.onDestroyView();
isCreated = false;
isPreVisible = false;
}
protected void onFragmentLoad() {}
protected void onFragmentLoadStop() {}
}
通过判断之前的状态,和后序的状态来决定事件的分发。
测试4:当上述还存在一个问题是,Fragment可能是其他Activity跳过来的,那么此时就不会走setUserVisibleHint(),因此需要在onResume和onPause中进行事件分发。
在测试3的基础上增加如下代码:
@Override
public void onResume() {
super.onResume();
if (!isPreVisible && getUserVisibleHint()) {
dispatchVisibleState(true);
}
}
@Override
public void onPause() {
super.onPause();
if (isPreVisible && !getUserVisibleHint()) {
dispatchVisibleState(false);
}
}
测试5:当LazyFragment嵌套其他Fragment时,当在onCreateView中加载子Fragment时,会导致还不可见就会被加载,因此需要进行过滤,只有当父可见时才进一步加载。
同时,由于Fragment嵌套,切过去无感知,需要手动分发一下
private void dispatchVisibleState(boolean isVisibleToUser) {
if (isPreVisible == isVisibleToUser) {
return;
}
isPreVisible = isVisibleToUser;
// 解决在initView中嵌套子Fragment的情况,导致还不可见就被加载的情况
// 有以下情形,在该Fragment的隔壁的LazyFragment中嵌套了ViewPager,在initView的时候初始化了ViewPager,进而使得子Fragment被提前加载,
// 需要增加如下判断
// 只有parentFragment可见时才加载
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
Fragment parentFragment = getParentFragment();
if (parentFragment instanceof LazyFragment01 && !parentIsVisible()) {
return;
}
}
if (isVisibleToUser) {
onFragmentLoad();
dispatchChildVisibleState(true); // Fragment嵌套时,切过去子Fragment无感知,需要手动分发一下
} else {
onFragmentLoadStop();
dispatchChildVisibleState(false);
}
}
private void dispatchChildVisibleState(boolean state) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
FragmentManager fragmentManager = getChildFragmentManager();
List<Fragment> fragments = fragmentManager.getFragments();
if (fragments != null) {
for (Fragment fragment : fragments) {
if (fragment instanceof LazyFragment01 && !fragment.isHidden() && fragment.getUserVisibleHint()) {
((LazyFragment01)fragment).dispatchVisibleState(state);
}
}
}
}
}
还还需注意的是要将可见状态分发给子Fragment。
ViewPager2
ViewPager2中的坑
ViewPager2的用法和ViewPager非常类似,FragmentStatePageAdapter换成了FragmentPageAdapter。但这里仍然有一些坑,如有时候会销毁前面的Fragment,有时候又不会。
具体表现为:当ViewPager2存在多个Fragment时,当访问前几个Fragment的时候,我们发现并不会调用前面的Fragment的销毁。当访问超过3个Fragment的时,开始陆续销毁前面的Fragment。特别的,当访问最后一个Fragment的时候,又不会销毁前面的Fragment了。
原因:RecyclerView的回收复用机制导致的。
1、由于RecyclerView不可见的item使用mCacheView缓存2个。因此,若访问超过3个item时,就会销毁之前的item。
2、由于RecyclerView预取的机制存在,使得在访问第i个item时,会向后预取第i+1个ViewHolder,同时缓存大小也会+1。因此在访问最后一个item时,由于缓存大小为3了,因此相当于前面可以缓存3个item,因此倒数第4个item不会被销毁。
ViewPager2懒加载的用法
1、使用。ViewPager2默认就实现了懒加载。由于ViewPager2的Fragment只有可见时才调用onResume方法,因此我们可以在onResume方法中进行数据加载,这就可以实现懒加载。
2、优化。同时,由于ViewPager2的缓存大小为2+1,因此最多只能缓存3个item,会频繁创建和销毁Fragment,因此我们可以通过调用mViewPager2.setOffscreenPageLimit(mFragments.size());,之后再在onResume中实现加载数据的逻辑。
参考:https://blog.csdn.net/qq_36486247/article/details/103959356