ViewPager缓存

引言

本文不再介绍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。
ViewPager缓存
当前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还没创建的部分。
但又引发了一个从未显示过的页面也将停止加载的问题。通过该方式,的确可以实现对界面懒加载,但不应该未展示过的界面出现停止加载,导致性能损耗。
出现该问题的原因如下:
ViewPager缓存
从上图可知,由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

上一篇:Fragment 简介


下一篇:安卓导航组件关于Navgation的使用细节与源码解读之基础知识(一)