相信技术的力量

ViewPager+Fragment组合的预加载和懒加载

预加载介绍

ViewPager+Fragment的搭配在日常开发中也比较常见,可用于切换展示不同类别的页面,我们日常所见的咨询、购物、金融、社交等类型的APP都有机会用到这种控件组合.

例如:

今日头条APP

ViewPager控件有个特有的预加载机制,即默认情况下当前页面左右两侧的1个页面会被加载,以方便用户滑动切换到相邻的界面时,可以更加顺畅的显示出来.

通过ViewPager的setOffscreenPageLimit(int limit)可以设置预加载页面数量,当前页面相邻的limit个页面会被预加载进内存.

效果如下:注意看Log输出

viewpager预加载2页

懒加载介绍

所谓的懒加载,其实也就是延迟加载,就是等到该页面的UI展示给用户时,再加载该页面的数据(从网络、数据库等),而不是依靠ViewPager预加载机制提前加载两三个,甚至更多页面的数据.这样可以提高所属Activity的初始化速度,也可以为用户节省流量.而这种懒加载的方式也已经/正在被诸多APP所采用.

但是通过ViewPager方法setOffscreenPageLimit(int limit)的源码可以发现,ViewPager通过一定的逻辑判断来确保至少会预加载左右两侧相邻的1个页面,也就是说无法通过简单的配置做到懒加载的效果.

ViewPager方法setOffscreenPageLimit(int limit) 相关源码

//默认的缓存页面数量(常量)
private static final int DEFAULT_OFFSCREEN_PAGES = 1;

//缓存页面数量(变量)
private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;

public void setOffscreenPageLimit(int limit) {
    //当我们手动设置的limit数小于默认值1时,limit值会自动被赋值为默认值1(即DEFAULT_OFFSCREEN_PAGES)
    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) {
        //经过前面的拦截判断后,将limit的值设置给mOffscreenPageLimit,用于
        mOffscreenPageLimit = limit;
        populate();
    }
}

关于变量mOffscreenPageLimit到底是什么.可以从其get方法注释中略见端倪

/**
 * 返回空闲状态下的视图层级中,当前页面任何一侧保存的页面数量,默认是1
 * Returns the number of pages that will be retained to either side of the
 * current page in the view hierarchy in an idle state. Defaults to 1.
 *
 * @return How many pages will be kept offscreen on either side
 * @see #setOffscreenPageLimit(int)
 */
public int getOffscreenPageLimit() {
    return mOffscreenPageLimit;
}

至于mOffscreenPageLimit到底是怎么影响ViewPager控件预加载的,暂不追查,因为此次的目的并不是ViewPager运行原理分析.

如何做到懒加载

既然通过ViewPager无法达到我们想要的懒加载效果,那么就得从Fragment自身入手了.

Fragment为我们提供了一个方法setUserVisibleHint(boolean isVisibleToUser),其中的参数isVisibleToUser就是表示该Fragment的UI对于用户是否可见

Fragment的方法 setUserVisibleHint(boolean isVisibleToUser)

/**
 * Set a hint to the system about whether this fragment's UI is currently visible
 * to the user. This hint defaults to true and is persistent across fragment instance
 * state save and restore.
 *
 * <p>An app may set this to false to indicate that the fragment's UI is
 * scrolled out of visibility or is otherwise not directly visible to the user.
 * This may be used by the system to prioritize operations such as fragment lifecycle updates
 * or loader ordering behavior.</p>
 *
 * <p><strong>Note:</strong> This method may be called outside of the fragment lifecycle.
 * and thus has no ordering guarantees with regard to fragment lifecycle method calls.</p>
 *
 * @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),
 *                        false if it is not.
 */
public void setUserVisibleHint(boolean isVisibleToUser) {
    if (!mUserVisibleHint && isVisibleToUser && mState < STARTED
            && mFragmentManager != null && isAdded()) {
        mFragmentManager.performPendingDeferredStart(this);
    }
    mUserVisibleHint = isVisibleToUser;
    mDeferStart = mState < STARTED && !isVisibleToUser;
}

大意就是通过此方法来设置Fragment的UI对用户是否可见,当该页面对用户可见/不可见时,系统都会回调此方法.

我们可以重写此方法,然后根据回调的isVisibleToUser参数来进行相关的逻辑判断,以达到懒加载的效果,比如如果isVisibleToUser==true的话表示当前Fragment对用户可见,此时再去加载页面数据.

由于ViewPager内会装载多个Fragment,而这种懒加载机制对于各个Fragment属于共同操作,因此适合将其抽取到BaseFragment中.

注意

setUserVisibleHint(boolean isVisibleToUser)方法会多次回调,而且可能会在onCreateView()方法执行完毕之前回调.如果isVisibleToUser==true,然后进行数据加载和控件数据填充,但是onCreateView()方法并未执行完毕,此时就会出现NullPointerException空指针异常.

基于以上原因,我们进行数据懒加载的时机需要满足两个条件

  1. onCreateView()方法执行完毕
  2. setUserVisibleHint(boolean isVisibleToUser)方法返回true

所以在BaseFragment中用两个布尔型标记来记录这两个条件的状态.只有同时满足了,才能加载数据

//Fragment的View加载完毕的标记
private boolean isViewCreated;

//Fragment对用户可见的标记
private boolean isUIVisible;

第一步,改变isViewCreated标记

onViewCreated()方法执行时,表明View已经加载完毕,此时改变isViewCreated标记为true,并调用lazyLoad()方法

@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    isViewCreated = true;
    lazyLoad();
}

第二步,改变isUIVisible标记

setUserVisibleHint(boolean isVisibleToUser)回调为true时,改变isUIVisible标记为true,并调用lazyLoad()方法

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);
    //isVisibleToUser这个boolean值表示:该Fragment的UI 用户是否可见
    if (isVisibleToUser) {
        isUIVisible = true;
        lazyLoad();
    } else {
        isUIVisible = false;
    }
}

第三步: 在lazyLoad()方法中进行双重标记判断,通过后即可进行数据加载

private void lazyLoad() {
    //这里进行双重标记判断,是因为setUserVisibleHint会多次回调,并且会在onCreateView执行前回调,必须确保onCreateView加载完毕且页面可见,才加载数据
    if (isViewCreated && isUIVisible) {
        loadData();
        //数据加载完毕,恢复标记,防止重复加载
        isViewCreated = false;
        isUIVisible = false;

        printLog(mTextviewContent+"可见,加载数据");
    }
}

第四步:定义抽象方法loadData(),具体加载数据的工作,交给子类去完成

protected abstract void loadData();

注意: 数据加载完毕要恢复标记,防止数据重复加载

效果如下:

Fragment懒加载示例

Demo源码

Github源码

⬆️