在一些选项卡滑动切换的应用场景下,我们大多会采用 ViewPager 与 Fragment 组合的方式来实现这样的需求,但 ViewPager 会预加载下一个 Framgment 的内容,这种机制的初衷是为了让用户更流畅的看到后面的内容。但是,当 ViewPager 中持久保留的 Framgment 数量增加,同时 Framgment 中又包含有大量的网络请求和数据库操作的时候,这种机制的弊端就出现了,预加载机制使的 ViewPager 内所有 Fragment 的网络请求和数据库操作同时被触发,CPU 和 内存占用会发生跳跃式的增长,最终造成 APP 卡顿,影响用户体验。
如果想提升用户体验,理想的状态大概是这样的:让 Fragment 内的网络请求和数据库操作根据需要进行触发。当 Fragment 对用户首次可见时,执行网络请求或数据库操作;当 Fragment 滑出屏幕时,不追加任何后续操作;当 Fragment 对用户再次可见时,根据首次执行状态决定是否执行网络请求或数据库操作。这样一个过程实际上就是 Fragment 的懒加载过程。
1. Fragment 懒加载原理
Fragment 懒加载的实现都离不开一个名为 setUserVisibleHint(boolean isVisibleToUser)
的方法,源码对这个方法的描述是:设置一个标记,说明此 Fragment 的 UI 当前是否对用户可见。如果参数 isVisibleToUser
为 true 则表示此 Fragment 的 UI 当前对用户是可见的,否则不可见。
根据 isVisibleToUser
标记,我们就可以控制 Fragment 加载数据的时机,即当 Fragment 的 UI 对用户可见时加载数据,不可见的则被动的等待用户触发。原理知道后,我们只需要重写 Fragment 中的 setUserVisibleHint(boolean isVisibleToUser)
即可,鉴于 ViewPager 中的每个 Fragment 可能会有不同的业务逻辑,所以建议将此方法的重写逻辑放到 Fragment 的基类中进行统一管控,Fragment 中 setUserVisibleHint(boolean isVisibleToUser)
方法的默认实现如下:
1 |
|
理论上,通过重写此方法实现数据加载逻辑,应该是没问题的,但这里忽略了一点:setUserVisibleHint(boolean isVisibleToUser)
方法的执行窗口在哪呢?也就是该方法什么时候执行,会不会在 Fragment 的生命周期里执行。如果在 Fragment 的生命周期里执行,那么它又排在哪个生命周期方法之前呢?同样,API 描述里也给出了如下提示:
注意:此方法可能会在 Fragment 的生命周期之外被调用。因此,相对于 Fragment 有序的生命周期来说,该方法是没有一个固定的执行窗口的。
这句话的是什么意思呢?意思就是说 setUserVisibleHint(boolean isVisibleToUser)
方法可能会在 Fragment 的生命周期之外或生命周期之内的任何一个时机被调用,总之就是调用时机不确定。但唯一能保证的是,当 Fragment 的 UI 对用户可见时,此方法一定会被调用。
那么,问题来了。如果此方法在 Fragment 开始执行自己的生命周期前被调用,会出现什么问题呢?假设此方法在 Fragment 的 onCreateView()
之前被调用呢(事实上绝大多数情况都是这样)?这时,Fragment 的控件还没有进行初始化,即使进行了网络请求或者数据库操作,在进行数据和控件绑定的时候,还是会发生空指针异常。
如何规避这种情况的发生呢?熟悉 Fragment 生命周期 的开发者可能已经想到了解决办法。既然 setUserVisibleHint()
的执行窗口不确定,唯一确定的就是 Fragment 的 UI 对用户可见时会被调用,那么,我们可以在 Fragment 基类设置一个 UI 是否初始化完毕的标签 isInitComplete
,只有 onCreateView()
方法执行完毕的时候,才将该标签的状态设置为初始化完毕。这样 setUserVisibleHint()
在 onCreateView()
执行前被调用的时候,可以通过判断 isInitComplete
标签来决定是否执行网络请求或数据库操作。如果 isInitComplete
为 true,则执行后续逻辑;否则,不执行任何操作。
这样的逻辑确实会避免空指针异常,但伴随的问题也非常明显。如果 isInitComplete
标签为 false,并且该 Fragment 又是首次对用户可见,那就意味着该 Fragment 会跳过网络请求逻辑。如果想再次触发 setUserVisibleHint()
方法,就只能等待用户滑动选项卡将此 Fragment 滑出视线,然后再将此 Fragment 滑入视线。
为了解决 Fragment 对用户首次可见时,跳过网络请求的问题。我们还是只能回到 Fragment 的生命周期上来,在 Fragment 生命周期的一个特定节点上将 setUserVisibleHint()
方法中跳过的逻辑进行补充执行。这个特定的节点需要满足以下两个条件:
- 在 Fragment 生命周期里只执行一次
- 执行窗口又刚好满足控件初始化完毕
Fragment 生命周期里满足这两个条件的最理想的方法是 onActivityCreated()
,因为它只会被执行一次,执行顺序又排在 onCreateView()
之后。这时,控件的初始化已经完成,所以,也不会出现空指针异常。现在,我们就可以将 setUserVisibleHint()
方法中跳过的网络请求逻辑加入到 onActivityCreated()
方法中,进行补充执行了。
2. Fragment 懒加载实现
2.1 基类实现
根据前面的分析,懒加载 Fragment 的基类编写如下,除了增加初始化结束标记之外,还在 onViewCreated 中增加了一个抽象的初始化方法,子类在继承的时候必须实现该方法,并在这个方法中完成各种初始化操作,结束后在基类中改变初始化结束标记状态。
1 | package com.sunzn.fragment.base; |
2.2 子类实现
下面是继承自 BaseFragment 的子类,子类中实现了 init()
方法,重写了 onActivityCreated()
和 onUserVisibleHints()
方法,并在这两个方法中进行数据加载。为了避免 onUserVisibleHints()
重复执行,这里引入了一个 isLoadSuccess 标记,如果数据加载成功,那么 Fragment 对用户再次可见时,也不会重新去加载数据。
1 | package com.sunzn.fragment.subs; |
2.3 页面实现
1 | package com.sunzn.fragment; |
2.4 实现效果
以下是 Fragment 的懒加载过程,第一个 Fragment 优先完成初始化,紧接着在 onActivityCreated 中补充执行数据加载,第二个、第三个 Fragment 完成初始化,等待用户触发。用户滑动到第二个 Fragment,触发 onUserVisibleHint 中的数据加载,用户滑动到第三个 Fragment,也触发数据加载。
1 | AudioFragment.onViewCreated() 初始化完毕 |
3. Fragment 懒加载案例
LazyFragment 案例:LazyFragment @ Github