学不动也要学!探究Fragment延迟加载的前世今生

马可没有菠萝 | 作者

承香墨影 | 校对

https://juejin.im/post/6844904033774206984 | 原文

当聊到 Fragment 配合 ViewPager 的使用的时候,不得不提 Fragment 的懒加载机制。而对于 Fragment 懒加载问题的处理,网上不乏相关的优秀文章。

但是由于 Fragment 生命周期的原因,使得懒加载问题的处理,并不是那么的优雅。显然 Google 也意识到了问题所在。因此,在 Androidx 的库中对于 Fragment 的生命周期状态的控制,进行了深度优化,使得我们更容易的,去管控 Fragment 的生命周期,也使得我们更容易的,去处理懒加载问题。这一切的前提条件,是我们要了解 Google 对于 Fragment 做了哪些优化。那么就让我们借此机会一起来探究一下吧!(懒加载称作延迟加载我觉得更贴切一些,所以下文就统称为延迟加载了)

一、Fragment延迟加载的前世

虽然本篇文章是对于 Fragment 新特性的探究,但是我觉得,写文章总要有个因果。也为了照顾一下,还不太了解什么是延迟加载的同学。我们还是先来了解一下延迟加载,顺便回顾一下 Fragment 延迟加载的旧方案。

1. Fragment延迟加载的意义?

首先,我们要搞清楚一个问题。「Fragment 延迟加载」中的「延迟」并不指的是延迟加载 Fragment,而是延迟加载 Fragment 中的数据。对于 Fragment 的使用通常我们会结合 ViewPager,ViewPager 会默认在当前页面的左右两边至少预加载一个页面以保证 ViewPager 的流畅性。

我们假设在 ViewPager 的所有 Fragment 中都存在网络请求,当我们打开这个页面的时候由于 ViewPager 的预加载原因,即使在其它 Fragment 不可见的情况下,也会去进行网络请求加载数据。而如果此时用户,根本就没有去滑动 ViewPager 就退出了应用或者切换到了其他页面。那么对于这个不可见的 Fragment 中的网络请求,岂不是既浪费了流量,也浪费了手机和服务器的性能?

那么此时有的同学就有问题了。你就不能在 Fragment 显示的时候去加载数据吗?问的好!在解答之前我们先来看下 Fragment 的生命周期。

想必这张图大家应该都非常熟悉了。当 Fragment 被预加载的时候,此 Fragment 的生命周期会从 onAttach() 执行到 onResume()。显然我们无法通过 Fragment 的生命周期,来控制 Fragment 的延迟加载。那么该怎么办呢?我们且往下看。

2. 如何处理Fragment延迟加载?

通过上一小节的分析,我们知道想要在 Fragment 的生命周期中,处理延迟加载的问题,显然是走不通的。所以想要处理 Fragment 的延迟加载就需要另想它法了。还好,在 Fragment 中为我们提供了一个  setUserVisibleHint(isVisibleToUser: Boolean) 的方法,此方法有一个 Boolean 类型的 isVisibleToUser 入参,其意义表示当前的 Fragment 是否对用户可见。因此,对于 Fragment 的延迟加载,我们便可以通过这个方法来展开。

既然要使用 setUserVisibleHint(isVisibleToUser: Boolean) 那么就应该知道这个方法的调用时机。

我写一个 ViewPager 嵌套 Fragment 的例子来打印下日志:

注:上图打印的日志中「position:0」表示当前 Fragment,「position:1」表示预加载的 Fragment,下同。

可见该方法是在 Fragment 的 onAttach() 之前就已经被调用了。因此,对于延迟加载我们可以在 setUserVisibleHint(isVisibleToUser: Boolean) 方法及 onViewCreated(view: View, savedInstanceState: Bundle?) 添加标志位来控制是否加载数据。

我们来看下代码:

abstract class BaseLazyFragment : Fragment() {
    /**
     * 当前Fragment状态是否可见
     */
    private var isVisibleToUser: Boolean = false
    /**
     * 是否已创建View
     */
    private var isViewCreated: Boolean = false
    /**
     * 是否第一次加载数据
     */
    private var isFirstLoad = true

    override fun setUserVisibleHint(isVisibleToUser: Boolean) {
        super.setUserVisibleHint(isVisibleToUser)
        this.isVisibleToUser = isVisibleToUser
        onLazyLoad()
    }

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        isViewCreated = true
        onLazyLoad()
    }

    protected abstract fun lazyLoad()
}

我们通过在 Fragment 中添加了三个标志位实现了延迟加载的功能。我们到 TestFragment 尝试一下:

class TestFragment : BaseLazyFragment() {
    private var position: Int = 0

    override fun setUserVisibleHint(isVisibleToUser: Boolean) {
        super.setUserVisibleHint(isVisibleToUser)
        val bundle = arguments
        position = bundle!!.getInt(KEY_POSITION)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val cardView = CardView(inflater, container)
        cardView.bind(Card.fromBundle(arguments!!),position)
        return cardView.view
    }

    companion object {

        private const val KEY_POSITION = "position"

        fun getInstance(card: Card, position: Int): TestFragment {
            val fragment = TestFragment()
            val bundle = card.toBundle()
            bundle.putInt(KEY_POSITION, position)
            fragment.arguments = bundle
            return fragment
        }
    }

    override fun lazyLoad() {
        showToast("Fragment$position is loading data")
    }

    private fun showToast(content: String) {
        Toast.makeText(context, content, Toast.LENGTH_SHORT).show()
    }
}

我们来看下效果:

嗯!立竿见影,只有当 Fragment 完全显示出来的时候 loading data 的操作才被执行。这种延迟加载的方案在 Androidx 1.1.0 版本以前被广泛应用。而在 Androidx 1.1.0 版本中,Google 对于 Fragment 进行了优化处理,使得延迟加载也有了新的解决方案。

二、Fragment的setMaxLifecycle()

上一节中我们讲到,因为 ViewPager 的预加载机制,以及 Fragment 的生命周期无法得以控制,我们不得不通过 setUserVisibleHint(isVisibleToUser: Boolean)onViewCreated(view: View, savedInstanceState: Bundle?) 方法,以及添加三个标志位来处理延迟加载,显然这样的代码并不优雅。

当我们将 Android 项目迁移到 Androidx 并将 Androidx 版本升级到 1.1.0 之后发现,我们第一节中用到的 setUserVisibleHint(isVisibleToUser: Boolean)方法已被标记为废弃了!

/**
 * ... 省略其它注释
 * @deprecated Use {@link FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.State)}
 * instead.
 */
@Deprecated
public void setUserVisibleHint(boolean isVisibleToUser) {
    if (!mUserVisibleHint && isVisibleToUser && mState < STARTED
            && mFragmentManager != null && isAdded() && mIsCreated) {
        mFragmentManager.performPendingDeferredStart(this);
    }
    mUserVisibleHint = isVisibleToUser;
    mDeferStart = mState < STARTED && !isVisibleToUser;
    if (mSavedFragmentState != null) {
        // Ensure that if the user visible hint is set before the Fragment has
        // restored its state that we don't lose the new value
        mSavedUserVisibleHint = isVisibleToUser;
    }
}

并且从注释中可以看到,使用 FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.State)  方法来替换 setUserVisibleHint()setMaxLifecycle() 是在 Androidx 1.1.0 中新增加的一个方法。

setMaxLifecycle() 从名字上来看,意思是设置一个最大的生命周期,因为这个方法定义在 FragmentTransaction 中,因此我们可以知道应该是为 Fragment 来设置一个最大的生命周期。我们来看下 setMaxLifecycle() 的源码:

/**
 * Set a ceiling for the state of an active fragment in this FragmentManager. If fragment is
 * already above the received state, it will be forced down to the correct state.
 *
 * <p>The fragment provided must currently be added to the FragmentManager to have it's
 * Lifecycle state capped, or previously added as part of this transaction. The
 * {@link Lifecycle.State} passed in must at least be {@link Lifecycle.State#CREATED}, otherwise
 * an {@link IllegalArgumentException} will be thrown.</p>
 *
 * @param fragment the fragment to have it's state capped.
 * @param state the ceiling state for the fragment.
 * @return the same FragmentTransaction instance
 */
@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;
    }
  }

而在 setMaxLifecycle() 中,接收的生命周期状态,要求不能低于 CREATED,否则会抛出一个 IllegalArgumentException 异常。当传入参数为 DESTROYED 或者 INITIALIZED 时则会抛出如下图的异常:

因此除去这两个生命周期外,仅剩下 CREATEDSTARTEDRESUMED 三个生命周期状态的参数可用,那么接下来我们就逐个来研究这三个参数的效果。

1. 不设置 setMaxLifecycle()

我们先来看下在不设置 setMaxLifecycle() 的时候添加一个 Fragment 的状态,以便和之后的情况进行对比。

首先我们在 Activity 中添加一个 Fragment,代码如下:

fragment = TestLifecycleFragment.getInstance(Card.DECK[0], 0)
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.ll_fragment, fragment)
fragmentTransaction.commit()

启动 Activity,我们将该 Fragment 生命周期的日志打印出来如下:

可以看到这个 Fragment 生命周期从 onAttach() 一直执行到了 onResume(),  并且在 Activity 中成功显示出了 Fragment。

2. setMaxLifecycle与CREATED

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

fragment = TestLifecycleFragment.getInstance(Card.DECK[0], 0)
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.ll_fragment, fragment)
fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.CREATED)
fragmentTransaction.commit()

再来看日志输出:

可以看到该 Fragment 的生命周期,仅仅执行到了 onCreate() 就没再往下执行了。并且 Activity 中没有加载出来当前 Fragment。

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

从日志中可以看到已经执行了 onResume() 的 Fragment,将其最大生命周期设置为 CREATED 后会执行 onPause → onStop → onDestoryView。也就是回退到了 onCreate() 的状态。

3. setMaxLifecycle与STARTED

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

fragment = TestLifecycleFragment.getInstance(Card.DECK[0], 0)
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.ll_fragment, fragment)
fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED)
fragmentTransaction.commit()

日志输出如下:

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

可以看到,设置最大生命周期 STARTED 后 Fragment 执行了 onPause() 方法,也就是生命周期退回到了 onStart()

4. setMaxLifecycle与RESUMED

最后,我们将 maxLifecycle 设置为 RESUMED

fragment = TestLifecycleFragment.getInstance(Card.DECK[0], 0)
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.ll_fragment, fragment)
fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED)
fragmentTransaction.commit()

可以看到此时,和第一种情况一样的效果,Fragment 的生命周期执行到了 onResume()

而对于已经执行了 onResume() 后的 Fragment,再去设置最大生命周期为 RESUMED 会怎么样呢?因为当前 Fragment 已经是 RESUMED 状态了,所以不会再去执行任何代码。到这里我们可以得出一个结论:

通过 setMaxLifecycle() 方法,可以精确控制 Fragment 生命周期的状态,如果 Fragment 的生命周期状态,小于被设置的最大生命周期,则当前 Fragment 的生命周期会执行到被设置的最大生命周期;反之,如果 Fragment 的生命周期状态大于被设置的最大生命周期,那么则会回退到被设置的最大生命周期。

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

三、Fragment延迟加载的今生

1、延迟加载新方案之于ViewPager

通过上一小节的分析,我们知道了可以通过 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 参数,其可选值如下:

@Retention(RetentionPolicy.SOURCE)
@IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT})
private @interface Behavior { }

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

所以我们的可选参数只剩下了 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
当 behavior 为 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 时意味着只有当前显示的 Fragment 会被执行到 onResume(),而其它 Fragment 的生命周期都只会执行到 onStart()

这一功能是如何实现的呢?我们追随 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 的脚步找到了 setPrimaryItem() 方法,这个方法的作用是设置 ViewPager 当前显示的 Item,其源码如下:

public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    Fragment fragment = (Fragment)object;
    if (fragment != mCurrentPrimaryItem) {
        if (mCurrentPrimaryItem != null) {
            mCurrentPrimaryItem.setMenuVisibility(false);
            if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
                if (mCurTransaction == null) {
                    mCurTransaction = mFragmentManager.beginTransaction();
                }
                mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
            } else {
                mCurrentPrimaryItem.setUserVisibleHint(false);
            }
        }
        fragment.setMenuVisibility(true);
        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;
    }
  }

这段代码非常简单易懂,mCurrentPrimaryItem 是当前正在显示的 item,fragment 是接下来要显示的 item。

可以看到当 mBehaviorBEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 时,mCurrentPrimaryItem 的最大生命周期被设置为了 STARTED,而 fragment 的最大生命周期则被设置为了 RESUMED

而当 mBehaviorBEHAVIOR_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 状态。

知道了这一结论后,我们再去进行懒加载的控制,是不是就异常简单了?此时我们只需要一个 flag 去标志是否是第一次加载数据就可以了。

因此,懒加载的实现可以如下:

abstract class TestLifecycleFragment : Fragment() {
    private var isFirstLoad = true

    override fun onResume() {
        super.onResume()
        if (isFirstLoad) {
          isFirstLoad = false
            loadData()
        }
    }

    abstract fun loadData()
}

2、延迟加载之于 ViewPager2

分析 ViewPager2 的 offScreenPageLimit() 时候得出过这样一个结论:

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() 中即可。相信随着 ViewPager2 的普及延迟加载的概念也会慢慢淡出开发者的视线。

那么如果为 ViewPager2 设置了 offScreenPageLimit(1),那结果会是怎样的呢?我们来看日志:

从日志中可以看到 ViewPager2 预加载了一个 Fragment,并且预加载的 Fragment 的生命周期仅仅执行到了 onStart()。所以此处我们可以猜测在 FragmentStateAdapter 中一定设置了 setMaxLifecycle(fragment, STARTED)

具体源码不贴了,大家可以自行查看。因此,此时处理懒加载问题其实和 ViewPager 的懒加载新方案如出一辙了,仅仅需要添加一个 boolean 值即可。

三、总结

本篇文章对于 Fragment 的延迟加载,进行了深入的探究,并且了解了在 Androidx 1.1.0 版本中对 Fragment 最大生命周期状态的控制,从而探究出了 Fragment 延迟加载的新方案。

对于 ViewPager2,因其默认不会进行预加载,因此也就意味着我们无需处理 ViewPager2 的延迟加载问题。好了,这一篇花费了作者两个周末的文章到此就结束了,如果你从中学有所收获那么请你不要吝啬留下你的赞。

热文推荐:

本文对你有帮助吗?留言、转发、点好看是最大的支持,谢谢!

公众号后台回复成长『成长』,将会得到我准备的学习资料。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值