本文不再介绍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的时候加载页面,那么就会白白浪费两个页面的内存。
因此,我们希望只在显示页面的时候加载页面,此时就需要用到懒加载。
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生命周期执行之前。
值得注意的是,这里的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的用法和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不会被销毁。
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