Fragment懒加载的实现方法(含源码分析)

一.背景介绍

       在android开发过程中,一个Activity里面可能会以Viewpager(或其他容器)与多个Fragment来组合使用(比如微信、轮播图、引导页)。而ViewPager默认会缓存三页数据,即:Viewpager每加载一个Fragment,都会预先加载此Fragment左侧或右侧的Fragment。而如果每个fragment都需要去加载数据,或从本地加载,或从网络加载,那么在这个activity刚创建的时候就变成需要初始化大量资源,浪费用户流量不止,还造成卡顿,这样的结果,我们当然不会满意。那么,能不能做到当切换到这个fragment的时候,它才去初始化呢?

二.fragment生命周期

1.默认的情况:

       为了更好的知道懒加载的实现原理,下面通过几个测试来进行逐步分析与突破。
       demo是一个Activity中使用ViewPager+4个fragment的形式,viewpager加载数据的方式采取的是系统模式的.生命周期对应的日志如下:

Fragment懒加载的实现方法(含源码分析)_第1张图片

(图1)
![图2](https://imgconvert.csdnimg.cn/aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTcwOTE3MjA0NjEwMjEw)
(图2)

       通过上面的生命周期日志,我们发现系统的确默认会进行缓存1个界面,即当前页面的相邻1个页面都会被缓存(从右往左滑动也是类似的)。

2.相关源码:

       对于viewPager实现缓存的源码如下:

......
private static final int DEFAULT_OFFSCREEN_PAGES = 1;
private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;
......
final int pageLimit = mOffscreenPageLimit;
.....
 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;
           .....
        }
    }

       这部分源码比较简单:如果你设置的缓存页面数量limit小于默认值1,就将1复制给limit;如果设置的缓存页面数量limit不等于1,将采用你设置的值作为缓存数目.由于setOffscreenPageLimit是从上往下执行的,if (limit != mOffscreenPageLimit)在这里其实是limit大于1.

3.改变limit的值:

(1)将limit赋值为2:
代码如下:

 ViewPager vp = (ViewPager) findViewById(R.id.viewpager);
......
 vp.setOffscreenPageLimit(2);

我们再来看看相关的生命周期方法.
Fragment懒加载的实现方法(含源码分析)_第2张图片

(图3)
设置limit为2时,通过生命周期发现的确加载出了相邻2个界面,达到了我们想要的结果. 将limit赋值为2、3或其他大于1的数,生命周期类似.这里就不贴出了. (2)将limit赋值为0:        将limit为0,我们主要是为了实现只加载当前的界面,即实现赖加载.但, 通过上述源码setOffscreenPageLimit方法及分析,我们可知:即使limit赋值为0,执行limit = DEFAULT_OFFSCREEN_PAGES后,limit还是变成了1.        调用vp.setOffscreenPageLimit(0)后查看日志,发现我们的发现是对的,即生命周期方法和默认情况一样. (3)如何实现赖加载呢?         既然limit为0不能实现赖加载,那应该如何实现呢?        细心的读者应该注意到了,除了打印生命周期方法外,还有setUserVisibleHint()方法.         由fragment的生命周期以及日志我们不难发现:除了第一次进入界面外,其他时候,我们处于某一个界面时,都不会调用该界面fragment的onCreateView、onActivityCreated等与fragment创建相关的方法,但会执行setUserVisibleHint().并且,只有处于某一个界面时,该界面fragment的setUserVisibleHint()的值为true,其他界面(无论是预加载的,还是没有预加载的)的setUserVisibleHint()值都为false.         无论是limit为1,还是0,或者是2,fragment都会调用fragment的setUserVisibleHint进行判断.那我们应该想到了尝试在setUserVisibleHint()上寻找解决方法. 我们先来看看fragment的setUserVisibleHint对应的源码:
public void setUserVisibleHint(boolean isVisibleToUser) {
        if (!mUserVisibleHint && isVisibleToUser && mState < STARTED&& mFragmentManager != null && isAdded()) {
    mFragmentManager.performPendingDeferredStart(this);
        }
        mUserVisibleHint = isVisibleToUser;
        mDeferStart = mState < STARTED && !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.
     *
     * 

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.

* *

Note: This method may be called outside of the fragment lifecycle. * and thus has no ordering guarantees with regard to fragment lifecycle method calls.

* * @param isVisibleToUser true if this fragment's UI is currently visible to the user (default), * false if it is not. */

注释描述得比较详细,总结起来就是:

1.当isVisibleToUser 为true则进行数据加载,当isVisibleToUser为false则不进行数据加载.
2.对于已经加载过数据的fragment,再次被滑动到也不在进行加载数据,也就是每个fragment仅做一次数据加载工作.

       有的人可能说,我的英语很菜,四级都没过,如果是这样的话,那就直接打开在线翻译工具,把对应的英语贴上去,就有对应的中文翻译.虽然翻译得不是那么顺畅,但基本意思还是看得出来的.

三.实现赖加载

       一般需要使用到fragment的情况,往往都是多个fragment一起使用.我们建立一个fragment的基类,然后在基类上进行操作(源码中的BaseLazyFragment).
       关于Base的封装,不属于本篇博客的讲解内容,这里将核心代码贴出来(完整代码见源码链接),并进行讲解:

onCreateView()方法:

 @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
            savedInstanceState) {
        convertView = inflater.inflate(getLayoutId(), container, false);
       ......
        initView();
        isInitView = true;
        lazyLoadData();
        return convertView;
    }

重写setUserVisibleHint()方法:

 if (isVisibleToUser) {
            isVisible = true;
            lazyLoadData();

        } else {
            isVisible = false;
        }
        super.setUserVisibleHint(isVisibleToUser);

如果某界面可见的话,加载数据;否则不进行加载.
加载数据的代码如下:

 private void lazyLoadData() {
        if (isFirstLoad) {
            Log.e(TAG, "第一次加载 " + " isInitView  " + isInitView + ".............." + "isVisible" + isVisible + ".............." + this.getClass().getSimpleName());
        } else {
            Log.e(TAG, "不是第一次加载 " + " isInitView  " + isInitView
                    + ".............." + "isVisible  " + isVisible + ".............." + this.getClass().getSimpleName());
        }
        if (!isFirstLoad || !isVisible || !isInitView) {
            Log.e(TAG, "不加载" + ".............." + this.getClass().getSimpleName());
            return;
        }

        Log.e(TAG, "完成数据第一次加载" + ".............." + this.getClass().getSimpleName());
        initData();
        isFirstLoad = false;
    }

上述代码同样使用了一个技巧,通过isFirstLoad的boolean值进行判断.!isFirstLoad || !isVisible || !isInitView 这三个条件只有满足一个,以后的代码就不执行.如果往下执行了,才初始化数据.
另外,有一个细节必须注意:
initView方法是在onCreateView中调用,而initData只有执行过onCreateView才会调用,这样的顺序安排就不会导致在initData中执行数据加载过程,找不到需要的view而报错。
相关日志信息如下图:
Fragment懒加载的实现方法(含源码分析)_第3张图片

(图4)

Fragment懒加载的实现方法(含源码分析)_第4张图片

(图5)

看完图4和图5,你是不是整个人都不好了?费了那么多劲重写setUserVisibleHint()方法,生命周期和默认情况(图1、图2)是一样的,并没有实现赖加载呀.别急呀,我们看一下BaseLazyFragment的日志:
Fragment懒加载的实现方法(含源码分析)_第5张图片

(图6)
![图7](https://imgconvert.csdnimg.cn/aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTcwOTE3MjI0OTUwNjUy)
(图7)
第一次滑到A界面,只有A界面完成了数据的加载,B界面没有加载(控件view却完成了初始化).说白一点:当前界面的工作都是正常进行的,相邻界面只初始化控件,但不加载数据.这不正是我们想要的吗? 当我们第二次进入该某个界面时,(一开始是A->B->C->D),以D->C为例,通过图8的日志可以看出,不仅相邻的B界面数据没有加载,连当前的C界面数据也没有加载. ![图8](https://imgconvert.csdnimg.cn/aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTcwOTE3MjI1MDI1OTQx)
(图8)

可以看出,fragment只会执行一次initData,从而实现懒加载功能这一需求.

四.源码下载

  • 点击下载源码
  • 您的star就是对我最大的鼓励!

你可能感兴趣的:(Android知识)