Fragment 数据懒加载及原理
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Fragment 数据懒加载及原理相关的知识,希望对你有一定的参考价值。
参考技术A最近据后台同事反馈说,某些接口调用的频率有点高,而这块业务还没完全开放,照理说很少会用到,于是让我查查怎么回事。
我看了下日志,把网络请求日志过滤出来,发现的确有问题,每次打开首页后都有许多那块业务相关的网络请求。于是马上联想到可能是因为首页改版之后嵌套使用了 ViewPager,业务未完全开放的那个 fragment 里嵌套了一个 ViewPager,里面有多个 fragment,这样每次打开首页都会去加载该 page,然后是一连串的 fragment 初始化以及网络请求,所以为了解决该问题就不得不使用懒加载。
最终想要实现的效果是:1) 当 fragment 不可见的时候不加载数据;2) 当数据已经加载过之后,除非手动刷新否则不重新请求数据。
首先,默认情况下,由于 ViewPager 会预加载左右两边相邻的 至少 1 个 fragment,通过 setOffscreenPageLimit() 设置预加载 page 数为 0 并不会起作用,这点从 ViewPager 的源码中可以看到:
从以上源码可以看出相邻 fragment 的加载是必然的,但是我们如果可以得知 fragment 可见性,那么就可以在 fragment 可见时才去加载数据。这样虽然不是完全的懒加载,只是数据懒加载,但是同样也可以满足我们的需求了。
那么 fragment 中有没有可以获取当前 fragment 是否可见的方法呢,当然是有的,它就是 setUserVisibleHint(boolean isVisibleToUser) 。
无论你使用的是 FragmentPagerAdapter 还是 FragmentStatePagerAdapter,当它们初始化 fragment 的时候,该方法都会被调用两次。
一次是在实例化的时候,也就是在 instantioateItem() 方法中:
一次是在用户滑动到当前 fragment 的时候,在 setPrimaryItem() 方法中:
另外,当用户从当前 fragment 滑出的时候,setPrimaryItem() 方法也会被调用。
来看下 setUserVisibleHint() 的注释:
系统正是通过该方法来判断当前 fragment 的 UI 是否对用户可见,而该方法被暴露出来的主要目的也是让我们可以提醒系统当前 fragment 已经不可见了,是时候重新更新 fragment 的生命周期了。
不过如果只是实现数据懒加载,我们不需要直接去调用该方法,只要覆写它并实现控制数据加载的逻辑就可以了。
这里我参考了一种比较简便的做法,原文来自 尹star 的 ViewPager+Fragment LazyLoad 最优解 。
实现效果: lazy_load_fragment_demo
项目地址: aJIEw/DemoUI-LazyLoadFragment
可以看到只有第一次进入 fragment 的时候才会加载数据,而且也不会主动加载相邻的 fragment 或者已经加载过的数据了。
首先,由于 setUserVisibleHint() 会在 fragment 实例化时就先被调用 (在 onAttach() 之前),所以我们最好在 view 创建完毕之后加载数据,因此需要设置一个 view 是否初始化完毕的标志位。另外,当然也需要一个 view 是否可见的标志位,只有等到 view 可见才允许加载。然后还可以选择保存数据的初始化状态,这样可以控制在 fragment 生命周期中的合适时机重新加载数据。所以,我们需要以下 3 个标志位:
然后接下来分为两种情况,一种是 view 初始化完毕但是此时还不可见的情况。很显然,我们只要判断 setUserVisibleHint() 中参数的值就可以了:
还有一种情况是,如果当前 fragment 是整个 ViewPager 的第一个 fragment,那么 setUserVisibleHint(true) 会在 view 初始化之前就在 setPrimaryItem() 中被调用,此时 view 已经可见了,但是我们要等到 view 初始化才加载数据,所以我们要在某个地方判断 view 是否已经初始化并且去加载数据。
最好的地方是在 onActivityCreated() 中。根据 fragment 生命周期我们知道,onActivityCreated() 会在 onCreateView() 之后调用,此时 view 已经初始化完毕,我们可以在这里将 isViewInitiated 标记为 true,同时在这里为第一个显示的 fragment 加载数据:
最后,我们还需要判断下数据是否已经加载过,避免重复加载。
我们将以上所有判断逻辑写在 prepareFetchData() 中,判断条件为 view 已经初始化、可见且数据未加载:
最后再定义一个抽象方法 fetchData(),让子类去实现:
这样一个完整的数据懒加载就实现完毕了。
我们可以看下以上操作的日志来验证下我们的想法。
第一次打开,FirstFragment 作为第一个可见的 fragment 立马被初始化:
此时 isVisibleToUser 会在 isViewInitiated 之前设为 true,所以 FirstFragment 会在 onActivityCreated() 中真正开始获取数据。
另外,由于预加载的存在,SecondFragment 也会被创建,但是此时还不可见:
当滑动到 SecondFragment 的时候,SecondFragment 状态变为可见,setUserVisibleHint(true) 被调用,所以开始获取数据:
而此时 FirstFragment 由可见变为不可见:
ThirdFragment 则开始第一次被创建,同样此时并不可见:
当滑动到 ThirdFragment 的时候,状态变为可见,所以也就开始获取数据:
此时 SecondFragment 由可见变为不可见:
而 FirstFragment 由于超出了 ViewPager 可以保存的 Fragment 的数量,所以被销毁:
此时 SecondFragment 重新变得可见:
而 FirstFragment 也开始重新被创建:
此时 FirstFragment 重新变得可见,虽然 FirstFragment 之前被销毁了,但是由于之前获取的数据会被恢复,所以现在不会重新去获取数据:
当然我们也可以选择在 onDestroy() 中将 isDataInitiated 置为 false,这样每次 fragment 重新创建都会重新获取数据。当然前提是你使用的是 FragmentStatePagerAdapter ,因为如果使用 FragmentPagerAdapter ,不会每次都调用 onDestroy(),fragment 实例会被保存。而 SecondFragment 再次变得不可见,ThirdFragment 被销毁,过程与 3 中移动到 ThirdFragment 类似,这里就不截图了。
通过以上日志,验证了我们的想法是对的。
另外,如果是 ViewPager 嵌套 ViewPager 其实效果也是一样的,如果不做特殊处理,相邻的 fragment 的会被加载,导致该 fragment 中的 ViewPager 会去加载其中的 fragment。
Fragment 官方懒加载源码分析
文章目录
- 本次分析Fragment 1.3.4版本,不同版本源码会不同!
- Fragment官方文档
dependencies
val fragment_version = "1.3.4"
// Java language implementation
implementation("androidx.fragment:fragment:$fragment_version")
// Kotlin
implementation("androidx.fragment:fragment-ktx:$fragment_version")
// Testing Fragments in Isolation
debugImplementation("androidx.fragment:fragment-testing:$fragment_version")
1.Fragment懒加载
1.1 什么是预加载
- ViewPager搭配Fragment使用时,Fragment 在没有显示的时候,其实就已经初始化操作了,这是为了用户更好的体验,在滑动ViewPager时,浏览当前页面,当左右其实已经初始化好了。
- ViewPager默认是预加载一页,就是左右各一页,可以使用 setOffscreenPageLimit 设置,但最少一页,设置越多,预加载就越多,比如设置2,左右各预加载2页面。
//默认一页
private static final int DEFAULT_OFFSCREEN_PAGES = 1;
public void setOffscreenPageLimit(int limit)
//小于1,会设置为1
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;
populate();
- 我们通过打印日志就能看出除了看到的第一个页面,生命周期从 onAttach 执行到 onResume ,看不到的第二个页面也会 onResume ;那我们平时除了初始化控件,还有网络请求,都被加载完成了,这就是默认的预加载。
- 这里注意一点,第一行 setUserVisibleHint 前面没有参数,那是因为 setUserVisibleHint比onAttach更早执行 ,所以拿不到bundle传递的数据。
- 如果滑到第二个页面,那第三个页面也会预加载。
- 滑到第三个页面,第四个页面预加载,而第一个页面销毁。
- 而这个时候再从三滑到二,二页面只执行 setUserVisibleHint ,没有执行onResume了,到这应该大概了解什么是预加载。
- 可以看到默认情况下,Fragment 的生命周期各个方法只会走一次,再次滑动回来也就是setUserVisibleHint 会回调信息。
1.2 什么是懒加载
- 预加载看起来是挺好的,但是预加载的越多,可能就会越卡顿,暂用着更多的内存和消耗更多的流量。
- 对于性能不错的机子,网络好的情况下,体验确实不错,但我们开发时需要考虑一下比较旧,或者是性能比较落后的机子,尽可能给他们也不错的体验。
- 懒加载就是滑到哪个页面,初始化哪个页面,滑走的页面就可以做一些暂停操作。
- 在使用 FragmentStatePagerAdapter 时使用两个参数的构造方法,第二个参数传入 FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT,就可实现懒加载了,先看看效果。
/*TestFragment*/
private fun initView()
/* FragmentActivity */
viewPager = root.findViewById(R.id.viewpager)
// 传入FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
// list是数据
testViewPagerAdapter = TestFragmentAdapter(childFragmentManager ,FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT,list)
viewPager.adapter = testViewPagerAdapter
viewPager.offscreenPageLimit = 1
/**
* 继承两个参数的FragmentStatePagerAdapter
*/
class TestFragmentAdapter(fragmentManager: FragmentManager, behavior:Int, list: MutableList<String>) :
FragmentStatePagerAdapter(fragmentManager, behavior)
}
- 这里可以看到跟没有懒加载的情况对比,只有第一个Fragment 走到 onResume,而第二个Fragment 只到 onStart。
- 如果滑到第二个页面,第一个页面走onPause,只有第二个页面走到 onResume ,第三个页面走到onStart,还有就是 setUserVisibleHint不执行了。
- 再滑到第三个页面,可以看到第一个销毁,第二个页面onPuase,第三个页面onResume , 第四个onStart。
- 第三个页面再回到第二页面,创建第一个页面,第二个页面onResume,第三个页面onPause,销毁第四个页面。
- 我们可以看到官方懒加载加了后,各个页面的生命周期onResume,onPause开始走起来了,而不是只走一次,setUserVisibleHint不再回调了。
2. 原理分析
2.1 populate 做了什么
- 在预加载,设置 setOffscreenPageLimit,我们就看到这个方法了,其实在ViewPager 的 onMeasure 也是执行这个方法,那他做了什么呢。
- 他其实就是做了初始化工作,启动事务,增加或者删除item,也就是Fragment,提交事务,设置了mBehavior ,一步一步看下去就是 FragmentTransaction.setMaxLifecycle 或者 Fragment.setUserVisibleHint。
- 提交事务,如果想要了解Fragment更具体的生命周期怎么走的,可以看:Fragment 生命周期源码分析。
2.2 addNewItem 增加item
- populate()首先就是增加新的节点,重点看 mAdapter.instantiateItem方法 ,这里 PagerAdapter 本身没做啥,调用的是子类的 instantiateItem 方法。
/*ViewPager*/
void populate(int newCurrentItem)
...
if (curItem == null && N > 0)
curItem = addNewItem(mCurItem, curIndex);
ItemInfo addNewItem(int position, int index)
ItemInfo ii = new ItemInfo();
ii.position = position;
ii.object = mAdapter.instantiateItem(this, position);
ii.widthFactor = mAdapter.getPageWidth(position);
if (index < 0 || index >= mItems.size())
mItems.add(ii);
else
mItems.add(index, ii);
return ii;
2.2 instantiateItem 方法
- 我们以FragmentStatePagerAdapter这子类分析一下。
- 这段代码就是获取 position 对应的Fragment,Fragment 的添加操作是需要事务的,倒数第五行这里就添加,如果有设置懒加载,就 setMaxLifecycle。
@SuppressWarnings("deprecation")
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position)
if (mFragments.size() > position)
Fragment f = mFragments.get(position);
if (f != null)
return f;
//获取事务
if (mCurTransaction == null)
mCurTransaction = mFragmentManager.beginTransaction();
...
fragment.setMenuVisibility(false);
//如果不是设置懒加载,就执行setUserVisibleHint
if (mBehavior == BEHAVIOR_SET_USER_VISIBLE_HINT)
fragment.setUserVisibleHint(false);
mFragments.set(position, fragment);
//事务add操作
mCurTransaction.add(container.getId(), fragment);
//懒加载
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
return fragment;
2.3 setMaxLifecycle
- 重点就是这里了,其实都是封装为Op对象,在后面Fragment生命周期限制就会用到。
@NonNull
public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment,
@NonNull Lifecycle.State state)
addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state));
return this;
2.4 destroyItem 销毁item
- 这里也很简单,ViewPager移除Item,就是Fragment 事务移除,调用子类的 destroyItem 。
/*ViewPager*/
void populate(int newCurrentItem)
...
mAdapter.destroyItem(this, pos, ii.object);
/*FragmentStatePagerAdapter*/
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object)
Fragment fragment = (Fragment) object;
if (mCurTransaction == null)
mCurTransaction = mFragmentManager.beginTransaction();
...
while (mSavedState.size() <= position)
mSavedState.add(null);
mSavedState.set(position, fragment.isAdded()
? mFragmentManager.saveFragmentInstanceState(fragment) : null);
mFragments.set(position, null);
//事务移除
mCurTransaction.remove(fragment);
if (fragment.equals(mCurrentPrimaryItem))
mCurrentPrimaryItem = null;
2.5 setPrimaryItem 设置当前item
- 这里设置当前显示主item生命周期最大为STARTED,或者是setUserVisibleHint(false),他马上就要被滑走了。
- 即将成为主item的Fragment,设置为RESUMED,或者是setUserVisibleHint(true)。
/*ViewPager*/
void populate(int newCurrentItem)
...
mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
/*FragmentStatePagerAdapter*/
@Override
@SuppressWarnings("ReferenceEquality", "deprecation")
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object)
Fragment fragment = (Fragment)object;
if (fragment != mCurrentPrimaryItem)
//当前itemFragment,被划走
if (mCurrentPrimaryItem != null)
mCurrentPrimaryItem.setMenuVisibility(false);
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)
if (mCurTransaction == null)
mCurTransaction = mFragmentManager.beginTransaction();
//如果有设置懒加载,设置为STARTED
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();
//如果有设置懒加载,设置为RESUMED
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
else
fragment.setUserVisibleHint(true);
mCurrentPrimaryItem = fragment;
2.6 finishUpdate 提交事务
- 最后提交事务就会执行Fragment 的各个生命周期了。
/*ViewPager*/
void populate(int newCurrentItem)
...
mAdapter.finishUpdate(this);
/*FragmentStatePagerAdapter*/
@Override
public void finishUpdate(@NonNull ViewGroup container)
if (mCurTransaction != null)
if (!mExecutingFinishUpdate)
try
mExecutingFinishUpdate = true;
//提交事务
mCurTransaction.commitNowAllowingStateLoss();
finally
mExecutingFinishUpdate = false;
mCurTransaction = null;
2.7 Fragment生命周期执行
- commit之后具体怎么,执行可以看Fragment 生命周期源码分析。
- 设置了MaxLifecycle ,带着这个行为去看Fragment 生命周期。
/*BackStackRecord*/
void executeOps()
final int numOps = mOps.size();
for (int opNum = 0; opNum < numOps; opNum++)
final Op op = mOps.get(opNum);
final Fragment f = op.mFragment;
...
switch (op.mCmd)
.
.
.
case OP_SET_MAX_LIFECYCLE:
mManager.setMaxLifecycle(f, op.mCurrentMaxState);
break;
if (!mReorderingAllowed && !FragmentManager.USE_STATE_MANAGER)
// Added fragments are added at the end to comply with prior behavior.
mManager.moveToState(mManager.mCurState, true);
- moveToState,这里面的 FragmentStateManager 才会是真正控制Fragment 的方法执行哪一步。
- 根据MaxLifecycle , 得到maxState ,返回后限制Fragment生命周期。
/**/FragmentManger
void moveToState(@NonNull Fragment f, int newState)
//这里跟官方提供的懒加载有关,可以限制fragment执行到哪个阶段
newState = Math.min(newState, fragmentStateManager.computeExpectedState());
...
/*FragmentStateManager*/
int computeExpectedState()
// If the FragmentManager is null, disallow changing the state at all
if (mFragment.mFragmentManager == null)
return mFragment.mState;
// Assume the Fragment can go as high as the FragmentManager's state
int maxState = mFragmentManagerState;
// Don't allow the Fragment to go above its max lifecycle state
switch (mFragment.mMaxState)
case RESUMED:
// maxState can't go any higher than RESUMED, so there's nothing to do here
break;
case STARTED:
maxState = Math.min(maxState, Fragment.STARTED);
break;
case CREATED:
maxState = Math.min(maxState, Fragment.CREATED);
break;
case INITIALIZED:
maxState = Math.min(maxState, Fragment.ATTACHED);
break;
default:
maxState = Math.min(maxState, Fragment.INITIALIZING);
...
2.8 简易流程图
3.参考
3.1 参考文章
androidx来袭,Fragment如何更简单的实现懒加载?
以上是关于Fragment 数据懒加载及原理的主要内容,如果未能解决你的问题,请参考以下文章