fragment懒加载的前世与今生

前言

Fragment懒加载是android开发中一个常见的问题,网上也有很多关于懒加载的文章,而我写这篇博客的原因是因为随着AndroidX和ViewPager2的出现,懒加载有了不一样的实现方式,所以写这篇文章以时间为脉络来介绍懒加载,让读者对懒加载有一个全面的认识,后面我统称懒加载为延时加载。

为什么要对Fragment做延迟加载?

首先,我们要搞清楚一个问题。“Fragment延迟加载“中的“延迟”并不指的是延迟加载Fragment,而是延迟加载Fragment中的数据。对于Fragment的使用通常我们会结合ViewPager。ViewPager会默认在当前页面的左右两边至少预加载一个页面以保证ViewPager的流畅性。我们假设在ViewPager的所有Fragment中都存在网络请求。当我们打开这个页面的时候由于ViewPager的预加载原因,即使在其它Fragment不可见的情况下也会去进行网络请求加载数据。而如果此时用户根本就没有去滑动ViewPager就退出了应用或者切换到了其他页面。那么对于这个不可见的Fragment中的网络请求岂不是既浪费了流量也浪费了手机和服务器的性能?
那么此时有的同学就有问题了。你就不能在Fragment显示的时候去加载数据吗?问的好!在解答之前我们先来看下Fragment的生命周期
在这里插入图片描述
想必这张图大家应该都非常熟悉了。当Fragment被预加载的时候,此Fragment的生命周期会从onAttach执行到onResume。显然我们无法通过Fragment的生命周期来控制Fragment的延迟加载。那么该怎么办呢?我们且往下看。

延时加载的方法

1.setUserVisibleHint

前面我们已经知道通过Fragment的生命周期来控制Fragment的延迟加载是不现实的,但是好在Fragment有一个判断fragment是否可见的回调方法setUserVisibleHint(boolean isVisibleToUser),这个方法中有一个isVisibleToUser的boolean类型的参数,其意义表示当前的Fragment是否对用户可见, 这个方法只有在使用ViewPager的时候才会被调用,所以只适用于ViewPager+Fragment的情况, 其他情况可以用onHiddenChanged方法,这个后面会介绍。我们来看一下setUserVisibleHint在什么时候会被调用(一个ViewPager加载两个Fragment),
在这里插入图片描述
可以看到setUserVisibleHint在onAttach之前就被调用了,关于setUserVisibleHint的调用时机可以看一下这篇文章:从源码角度理解Fragment的setUserVisibleHint方法的妙用,并且已经加载过的fragment在切换时不会在执行任何生命周期的方法了。

因为setUserVisibleHint在onAttach之前就被调用了,我们不能在Fragment还没有加载前就去加载数据。因此,对于延迟加载我们可以在setUserVisibleHint(isVisibleToUser: Boolean)方法及onViewCreated(view: View, savedInstanceState: Bundle?)添加标志位来控制是否加载数据。来看一下代码:

public abstract class BaseFragment extends Fragment {
    /**
     * 当前Fragment状态是否可见
     */
    private boolean isVisibleToUser = false;
    /**
     * 是否已创建View
     */
    private boolean isViewCreated = false;
    /**
     * 是否第一次加载数据
     */
    private boolean isFirstLoad = true;

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        this.isVisibleToUser = isVisibleToUser;
        onLazyLoad();
    }

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

    private void onLazyLoad() {
        if (isVisibleToUser && isViewCreated && isFirstLoad) {
            isFirstLoad = false;
            lazyLoad();
        }
    }

    public abstract void lazyLoad();
}

代码我就不分析了,使用时只需将fragment继承这个BaseFragment,然后实现lazyLoad方法并在该方法中加载数据就可以实现延时加载了。

2.onHiddenChanged

前面我们说了setUserVisibleHint只有在使用ViewPager的时候才会被调用,那么如果是通过FragmentTransaction 的add hide show方法来控制fragment的显示隐藏该怎么实现延时加载呢?答案就是使用onHiddenChanged(boolean hidden)方法,从名字上判断这个方法和setUserVisibleHint有着异曲同工之妙,他的官方 API 的注释如下:

当 Fragment 隐藏的状态发生改变时,该函数将会被调用,如果当前 Fragment 隐藏, hidden 的值为 true, 反之为 false。最为重要的是hidden 的值,可以通过调用 isHidden() 函数获取。

onHiddenChanged方法是由FragmentTransaction来调用的,并且第一次载入显示,以及销毁时不会走onHiddenChanged方法。

public abstract class BaseFragment extends Fragment {
    /**
     * 当前Fragment状态是否可见
     */
    private boolean isVisibleToUser = false;
    /**
     * 是否已创建View
     */
    private boolean isViewCreated = false;
    /**
     * 是否第一次加载数据
     */
    private boolean isFirstLoad = true;

    @Override
    public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        this.isVisibleToUser = !hidden;
        onLazyLoad();
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        this.isVisibleToUser = isHidden();
        isViewCreated = true;
        onLazyLoad();
    }

    private void onLazyLoad() {
        if (isVisibleToUser && isViewCreated && isFirstLoad) {
            isFirstLoad = false;
            lazyLoad();
        }
    }

    public abstract void lazyLoad();
}

3.setMaxLifecycle

在Androidx 1.1.0版本中,Google对于Fragment进行了优化处理,使得延迟加载也有了新的解决方案。我们再在Fragment中使用setUserVisibleHint方法会发现该方法已经被废弃了:
在这里插入图片描述
在这里插入图片描述
通过setUserVisibleHint的注释我们可以知道setUserVisibleHint被FragmentTransaction的setMaxLifecycle方法代替,setMaxLifecycle是在Androidx 1.1.0中新增加的一个方法。setMaxLifecycle从名字上来看意思是设置一个最大的生命周期,因为这个方法是在FragmentTransaction中,因此我们可以知道应该是为Fragment来设置一个最大的生命周期。我们来看下setMaxLifecycle的源码:

    @NonNull
    public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment,
            @NonNull Lifecycle.State state) {
        addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state));
        return this;
    }

这个方法接收一个Fragment参数和一个Lifecycle的状态参数。Lifecycle是jetpack中很重要的一个库,它具有对Activity和Fragment生命周期感知能力,相信很多同学都应该对Lifecycle都略知一二。在Lifecycle的State中定义了五种生命周期状态,如下:

   public enum State {
        /**
         * Destroyed state for a LifecycleOwner. After this event, this Lifecycle will not dispatch
         * any more events. For instance, for an {@link android.app.Activity}, this state is reached
         * <b>right before</b> Activity's {@link android.app.Activity#onDestroy() onDestroy} call.
         */
        DESTROYED,

        /**
         * Initialized state for a LifecycleOwner. For an {@link android.app.Activity}, this is
         * the state when it is constructed but has not received
         * {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} yet.
         */
        INITIALIZED,

        /**
         * Created state for a LifecycleOwner. For an {@link android.app.Activity}, this state
         * is reached in two cases:
         * <ul>
         *     <li>after {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} call;
         *     <li><b>right before</b> {@link android.app.Activity#onStop() onStop} call.
         * </ul>
         */
        CREATED,

        /**
         * Started state for a LifecycleOwner. For an {@link android.app.Activity}, this state
         * is reached in two cases:
         * <ul>
         *     <li>after {@link android.app.Activity#onStart() onStart} call;
         *     <li><b>right before</b> {@link android.app.Activity#onPause() onPause} call.
         * </ul>
         */
        STARTED,

        /**
         * Resumed state for a LifecycleOwner. For an {@link android.app.Activity}, this state
         * is reached after {@link android.app.Activity#onResume() onResume} is called.
         */
        RESUMED;

        /**
         * Compares if this State is greater or equal to the given {@code state}.
         *
         * @param state State to compare with
         * @return true if this State is greater or equal to the given {@code state}
         */
        public boolean isAtLeast(@NonNull State state) {
            return compareTo(state) >= 0;
        }
    }

从低到高分别为DESTROYED、INITIALIZED、CREATED、STARTED、RESUMED;而在setMaxLifecycle中接收的生命周期状态要求不能低于CREATED,否则会抛出一个IllegalArgumentException的异常。因此除去DESTROYED、INITIALIZED两个生命周期外,仅剩下CREATED、STARTED、RESUMED三个生命周期状态的参数可用,

  • CREATED即已创建状态,狭义的理解是生命周期方法走到onCreate,如果当前fragment状态已大于CREATED,则会使fragment生命周期方法走到onDestoryView,如果小于CREATED,则走到onCreate;所以CREATED有两种情况;
  • 同理,STARTED状态也有两种情况,如果当前fragment状态已大于STARTED,则会使fragment生命周期方法走到onPause,如果小于CREATED,则走到onStart;
  • RESUMED表示的状态比较特殊,只代表onResume状态,无论大到小还是小到大,最终都是停留到onResume状态;

那么接下来我们就逐个来研究这三个参数的效果。

1.不设置setMaxLifecycle

我们先来看下在不设置setMaxLifecycle的时候添加一个Fragment的状态,以便和后边的情况进行对比。首先我们在Activity中添加一个Fragment,代码如下:

        FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
        fragmentTransaction.add(R.id.frameLayout, BaseFragment.newInstance());
        fragmentTransaction.commit();

启动Activity,我们将该Fragment生命周期的日志打印出来如下:
在这里插入图片描述
可以看到这个Fragment生命周期从onAttach一直执行到了onResume。并且在Activity中成功显示出了Fragment。

2.setMaxLifecycle为CREATED

接下来,我们将maxLifecycle设置为CREATED:

        FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
        Fragment fragment = BaseFragment.newInstance();
        fragmentTransaction.add(R.id.frameLayout, fragment);
        fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.CREATED);
        fragmentTransaction.commit();

在这里插入图片描述
可以看到该Fragment的生命周期仅仅执行到了onCreate就没再往下执行了。并且Activity中没有加载出来当前Fragment。

那么现在问题来了,假设Fragment已经执行到了onResume,此时再为Fragment设置一个CREATED的最大生命周期会出现什么样的情况呢?我们通过日志来验证一下:

        fragmentManager = getSupportFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragment = BaseFragment.newInstance();
        fragmentTransaction.add(R.id.frameLayout, fragment);
        fragmentTransaction.commit();
        findViewById(R.id.textView).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                fragmentManager.beginTransaction().setMaxLifecycle(fragment, Lifecycle.State.CREATED).commit();
            }
        });

在这里插入图片描述
从日志中可以看到已经执行了onResume的Fragment,将其最大生命周期设置为CREATED后会执行onPause->onStop->onDestoryView。在这里我纠结了很久,搞不懂为什么到了onDestoryView,其实如果我们设置为CREATED后再设置为STARTED,通过这个变化就可以理解了:
在这里插入图片描述
其实执行到onDestoryView就相当于回退到了CREATED,设置STARTED的时候正好是从onCreate后面的onCreateView执行的,如果我们从一开始就设置为CREATED后再设置为STARTED,那么就不会经过onResume阶段而是停在onStart。

3.setMaxLifecycle为STARTED

接下来,我们将maxLifecycle设置为STARTED:

        fragmentManager = getSupportFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragment = BaseFragment.newInstance();
        fragmentTransaction.add(R.id.frameLayout, fragment);
        fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
        fragmentTransaction.commit();

在这里插入图片描述
可以看到Fragment的生命周期执行到了onStart,并且Activity中成功显示出了当前fragment。
同样,假设Fragment已经执行到了onResume方法再为其设置最大生命周期为STARTED会怎样呢?来看日志:

        fragmentManager = getSupportFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragment = BaseFragment.newInstance();
        fragmentTransaction.add(R.id.frameLayout, fragment);
        fragmentTransaction.commit();
        findViewById(R.id.textView).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                fragmentManager.beginTransaction().setMaxLifecycle(fragment, Lifecycle.State.STARTED).commit();
            }
        });

在这里插入图片描述
可以看到,设置最大生命周期STARTED后Fragment执行了onPause方法。

4.setMaxLifecycle为RESUMED

最后,我们将maxLifecycle设置为RESUMED:

        fragmentManager = getSupportFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragment = BaseFragment.newInstance();
        fragmentTransaction.add(R.id.frameLayout, fragment);
        fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED).commit();
        fragmentTransaction.commit();

在这里插入图片描述
可以看到此时和第一种情况一样的效果,Fragment的生命周期执行到了onResume。
而对于已经执行了onResume后的Fragment,再去设置最大生命周期为RESUMED会怎么样呢?因为当前Fragment已经是RESUMED状态了,所以不会再去执行任何代码。

有了上面的结论,在ViewPager中便可以对Fragment的生命周期进行控制,以此来更方便的实现延迟加载功能了。

5.具体实现

通过上一小节的分析我们知道了可以通过setMaxLifecycle来设置Fragment的最大生命周期,从而可以实现ViewPager中Fragment的延迟加载。当然,关于生命周期状态处理的操作无需我们自己实现,在Androidx 1.1.0版本中的FragmentStatePagerAdapter已经帮我们实现了,只需要在使用时候传进去相应的参数即可。
FragmentStatePagerAdapter的构造方法接收两个参数,如下:

    /**
     * Constructor for {@link FragmentStatePagerAdapter}.
     *
     * If {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} is passed in, then only the current
     * Fragment is in the {@link Lifecycle.State#RESUMED} state, while all other fragments are
     * capped at {@link Lifecycle.State#STARTED}. If {@link #BEHAVIOR_SET_USER_VISIBLE_HINT} is
     * passed, all fragments are in the {@link Lifecycle.State#RESUMED} state and there will be
     * callbacks to {@link Fragment#setUserVisibleHint(boolean)}.
     *
     * @param fm fragment manager that will interact with this adapter
     * @param behavior determines if only current fragments are in a resumed state
     */
    public FragmentStatePagerAdapter(@NonNull FragmentManager fm,
            @Behavior int behavior) {
        mFragmentManager = fm;
        mBehavior = behavior;
    }

第一个FragmentManager 参数不必多说,第二个参数时一个枚举类型的Behavior参数,其可选值如下:

    @Deprecated
    public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0;

    public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1;

当behavior为BEHAVIOR_SET_USER_VISIBLE_HINT时和第一种方案一样,Fragment改变的时候,setUserVisibleHint方法会被调用,也就是这个参数其实是为了兼容以前的老代码。并且BEHAVIOR_SET_USER_VISIBLE_HINT参数已经被置为废弃。

当behavior为BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT时意味着只有当前显示的Fragment会被执行到onResume,而其它Fragment的生命周期都只会执行到onStart,并且在切换fragment的时候,正在显示的framgent会从onResume执行到onPause,将要显示的fragment会从onStart执行到onResume。这样的话只需要将加载的代码写在onResume中并加一个初次加载判断就可以实现延时加载。

public abstract class BaseFragment extends Fragment {

    private boolean isFirstLoad = true;
    
    @Override
    public void onResume() {
        super.onResume();
        if (isFirstLoad) {
            isFirstLoad = false;
        }
    }

    public abstract void lazyLoad();
}

FragmentStatePagerAdapter是怎么通过setMaxLifecycle帮我们实现这个功能的呢?看源码:

    public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        //将要切换的fragment
        Fragment fragment = (Fragment) object;
        //设置正在显示的fragment
        // 如果是BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT就使用setMaxLifecycle
        // 如果是BEHAVIOR_SET_USER_VISIBLE_HINT使用setUserVisibleHint
        if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
            if (mCurTransaction == null) {
                mCurTransaction = mFragmentManager.beginTransaction();
            }
            mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
        } else {
            mCurrentPrimaryItem.setUserVisibleHint(false);
        }
        //设置将要切换的fragment
        // 如果是BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT就使用setMaxLifecycle
        // 如果是BEHAVIOR_SET_USER_VISIBLE_HINT使用setUserVisibleHint
        if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
            if (mCurTransaction == null) {
                mCurTransaction = mFragmentManager.beginTransaction();
            }
            mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
        } else {
            fragment.setUserVisibleHint(true);
        }

        mCurrentPrimaryItem = fragment;
    }

这个方法是viewPager控制切换fragment时调用的代码,为了让代码更加易懂我进行了调整,mCurrentPrimaryItem是当前正在显示的item,fragment是接下来要显示的item。可以看到当mBehavior 为BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT时,mCurrentPrimaryItem的最大生命周期被设置为了STARTED,而fragment的最大生命周期则被设置为了RESUMED。而当mBehavior为BEHAVIOR_SET_USER_VISIBLE_HINT时仍然会调用setUserVisibleHint方法,这种情况就不再讨论,因为BEHAVIOR_SET_USER_VISIBLE_HINT也已经被废弃掉了。

那么我们着重来分析一下BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT时的情况:
mCurrentPrimaryItem是当前显示的Fragment,所以该Fragment必然已经执行到了onResume方法,而此时为其设置了最大生命周期STARTED,那么mCurrentPrimaryItem必然会执行onPause退回到STARTED状态。而fragment当前生命周期状态为onStart,当为其设置了RESUME的最大生命周期状态后,fragment必然会执行onResume方法进入RESUMED状态。

4.ViewPager2

ViewPager2的offScreenPageLimit默认值为OFFSCREEN_PAGE_LIMIT_DEFAULT,当setOffscreenPageLimit为OFFSCREEN_PAGE_LIMIT_DEFAULT时候会使用RecyclerView的缓存机制。默认只会加载当前显示的Fragment,而不会像ViewPager一样至少预加载一个item.当切换到下一个item的时候,当前Fragment会执行onPause方法,而下一个Fragment则会从onCreate一直执行到onResume。当再次滑动回第一个页面的时候当前页面同样会执行onPuase,而第一个页面会执行onResume。

也就是说在ViewPager2中,默认关闭了预加载机制。没有了预加载机制再谈延迟加载其实也没有任何意义了。所以关于ViewPager2的延迟加载也就不用多说了吧?只需要将网络请求放到onStart中即可,或者像上面一样将加载的代码写在onResume中并加一个初次加载判断。

总结

到这里延时加载的前世今生算是讲完了。。。。。。。。
参考:
Fragment新功能,setMaxLifecycle了解一下
Androidx 下 Fragment 懒加载的新实现

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值