Fragment 数据懒加载及原理

Blow, Blow Thou Winter Wind - John Everett Millais

Fragment 的懒加载算是比较常见的功能了,但是之前一直没有仔细研究过,直到最近有这方面的需求,所以就写下这篇文章记录下自己的探索过程。

起因

最近据后台同事反馈说,某些接口调用的频率有点高,而这块业务还没完全开放,照理说很少会用到,于是让我查查怎么回事。

我看了下日志,把网络请求日志过滤出来,发现的确有问题,每次打开首页后都有许多那块业务相关的网络请求。于是马上联想到可能是因为首页改版之后嵌套使用了 ViewPager,业务未完全开放的那个 fragment 里嵌套了一个 ViewPager,里面有多个 fragment,这样每次打开首页都会去加载该 page,然后是一连串的 fragment 初始化以及网络请求,所以为了解决该问题就不得不使用懒加载。

最终想要实现的效果是:1) 当 fragment 不可见的时候不加载数据;2) 当数据已经加载过之后,除非手动刷新否则不重新请求数据。

预加载与 setUserVisibleHint()

首先,默认情况下,由于 ViewPager 会预加载左右两边相邻的至少 1 个 fragment,通过 setOffscreenPageLimit() 设置预加载 page 数为 0 并不会起作用,这点从 ViewPager 的源码中可以看到:

private static final int DEFAULT_OFFSCREEN_PAGES = 1;
// ...
public void setOffscreenPageLimit(int limit) {
    if (limit < DEFAULT_OFFSCREEN_PAGES) { // <0? 对不起,不可以!
        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();
    }
}

从以上源码可以看出相邻 fragment 的加载是必然的,但是我们如果可以得知 fragment 可见性,那么就可以在 fragment 可见时才去加载数据。这样虽然不是完全的懒加载,只是数据懒加载,但是同样也可以满足我们的需求了。

那么 fragment 中有没有可以获取当前 fragment 是否可见的方法呢,当然是有的,它就是 setUserVisibleHint(boolean isVisibleToUser)

无论你使用的是 FragmentPagerAdapter 还是 FragmentStatePagerAdapter,当它们初始化 fragment 的时候,该方法都会被调用两次。

一次是在实例化的时候,也就是在 instantioateItem() 方法中:

public Object instantiateItem(ViewGroup container, int position) {}

一次是在用户滑动到当前 fragment 的时候,在 setPrimaryItem() 方法中:

public void setPrimaryItem(ViewGroup container, int position, Object object) {}

另外,当用户从当前 fragment 滑出的时候,setPrimaryItem() 方法也会被调用。

来看下 setUserVisibleHint() 的注释:

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.

系统正是通过该方法来判断当前 fragment 的 UI 是否对用户可见,而该方法被暴露出来的主要目的也是让我们可以提醒系统当前 fragment 已经不可见了,是时候重新更新 fragment 的生命周期了。

不过如果只是实现数据懒加载,我们不需要直接去调用该方法,只要覆写它并实现控制数据加载的逻辑就可以了。

这里我参考了一种比较简便的做法,原文来自 尹starViewPager+Fragment LazyLoad 最优解

实现效果:lazy_load_fragment_demo

项目地址:aJIEw/DemoUI-LazyLoadFragment

可以看到只有第一次进入 fragment 的时候才会加载数据,而且也不会主动加载相邻的 fragment 或者已经加载过的数据了。

如何实现及其原理(Koltin 实现)

首先,由于 setUserVisibleHint() 会在 fragment 实例化时就先被调用 (在 onAttach() 之前),所以我们最好在 view 创建完毕之后加载数据,因此需要设置一个 view 是否初始化完毕的标志位。另外,当然也需要一个 view 是否可见的标志位,只有等到 view 可见才允许加载。然后还可以选择保存数据的初始化状态,这样可以控制在 fragment 生命周期中的合适时机重新加载数据。所以,我们需要以下 3 个标志位:

/**
 * View 的初始化状态,只有初始化完毕才加载数据
 */
private var isViewInitiated: Boolean = false

/**
 * View 是否可见,只有可见时才去加载数据
 */
private var isVisibleToUser: Boolean = false

/**
 * 数据是否已经初始化,避免重复请求数据
 */
private var isDataInitiated: Boolean = false

然后接下来分为两种情况,一种是 view 初始化完毕但是此时还不可见的情况。很显然,我们只要判断 setUserVisibleHint() 中参数的值就可以了:

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

还有一种情况是,如果当前 fragment 是整个 ViewPager 的第一个 fragment,那么 setUserVisibleHint(true) 会在 view 初始化之前就在 setPrimaryItem()中被调用,此时 view 已经可见了,但是我们要等到 view 初始化才加载数据,所以我们要在某个地方判断 view 是否已经初始化并且去加载数据。

最好的地方是在 onActivityCreated() 中。根据 fragment 生命周期我们知道,onActivityCreated() 会在 onCreateView() 之后调用,此时 view 已经初始化完毕,我们可以在这里将 isViewInitiated 标记为 true,同时在这里为第一个显示的 fragment 加载数据:

override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    isViewInitiated = true
    prepareFetchData()
}

最后,我们还需要判断下数据是否已经加载过,避免重复加载。

我们将以上所有判断逻辑写在 prepareFetchData() 中,判断条件为 view 已经初始化、可见且数据未加载:

private fun prepareFetchData() {
    if (isViewInitiated && isVisibleToUser && !isDataInitiated) {
        isDataInitiated = true
        fetchData()
    }
}

最后再定义一个抽象方法 fetchData(),让子类去实现:

/**
 * 在该方法中发起网络请求获取数据
 */
abstract fun fetchData()

这样一个完整的数据懒加载就实现完毕了。

验证想法

我们可以看下以上操作的日志来验证下我们的想法。

1. 首次打开

第一次打开,FirstFragment 作为第一个可见的 fragment 立马被初始化:

1.1-FirstFragment_creation.png

此时 isVisibleToUser 会在 isViewInitiated 之前设为 true,所以 FirstFragment 会在 onActivityCreated() 中真正开始获取数据。

另外,由于预加载的存在,SecondFragment 也会被创建,但是此时还不可见:

1.2-SecondFragment_creation.png

2. 移动到 SecondFragment

当滑动到 SecondFragment 的时候,SecondFragment 状态变为可见,setUserVisibleHint(true) 被调用,所以开始获取数据:

2.1-SecondFragment_visible.png

而此时 FirstFragment 由可见变为不可见:

2.2-FirstFragment_not_visible.png

ThirdFragment 则开始第一次被创建,同样此时并不可见:

2.3-ThirdFragment_creation.png

3. 移动到 ThirdFragment

当滑动到 ThirdFragment 的时候,状态变为可见,所以也就开始获取数据:

3.1-ThirdFragment_visible.png

此时 SecondFragment 由可见变为不可见:

3.2-SecondFragment_not_visible.png

而 FirstFragment 由于超出了 ViewPager 可以保存的 Fragment 的数量,所以被销毁:

3.3-FirstFragment_destroyed.png

4. 回到 SecondFragment

此时 SecondFragment 重新变得可见:

4.1-SecondFragment_visible_again.png

而 FirstFragment 也开始重新被创建:

4.2-FirstFragment_recreation.png

5. 回到 FirstFragment

此时 FirstFragment 重新变得可见,虽然 FirstFragment 之前被销毁了,但是由于之前获取的数据会被恢复,所以现在不会重新去获取数据:

5.1-FirstFragment_visible_again.png

当然我们也可以选择在 onDestroy() 中将 isDataInitiated 置为 false,这样每次 fragment 重新创建都会重新获取数据。当然前提是你使用的是 FragmentStatePagerAdapter,因为如果使用 FragmentPagerAdapter,不会每次都调用 onDestroy(),fragment 实例会被保存。而 SecondFragment 再次变得不可见,ThirdFragment 被销毁,过程与 3 中移动到 ThirdFragment 类似,这里就不截图了。

通过以上日志,验证了我们的想法是对的。

另外,如果是 ViewPager 嵌套 ViewPager 其实效果也是一样的,如果不做特殊处理,相邻的 fragment 的会被加载,导致该 fragment 中的 ViewPager 会去加载其中的 fragment。

嵌套使用 ViewPager 情况下
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343