文章目录
ViewPager实现Fragment的懒加载
ViewPager
相信大家用的都很熟练了,但是原生的ViewPager1
是存在一些问题的,这篇文章就对ViewPager1
进行性能优化。
在优化之前,先看普通的使用存在什么问题。此篇文章使用Kotlin
,笔者刚从Java
转到Kotlin
,用的不熟练的地方请多指教。
对于安卓首推的开发语言,笔者认为转Kotlin
是非常有必要的,笔者之前记录了自己的学习Kotlin
的过程,详情请看链接。
ViewPager使用(未优化版本)
编写基础版本代码
首先是MainActivity
的布局,上部分是ViewPager
,下部分是关联的导航栏
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.viewpager.widget.ViewPager
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="0dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="0dp"
android:layout_marginBottom="19dp"
app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:id="@+id/viewPager">
</androidx.viewpager.widget.ViewPager>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigationView"
android:layout_width="0dp"
android:layout_height="78dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="5dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/viewPager"
app:menu="@menu/bottomnavigationview">
</com.google.android.material.bottomnavigation.BottomNavigationView>
</androidx.constraintlayout.widget.ConstraintLayout>
下一步编写Fragment
的布局文件,非常简单只有一个TextView
fragment_test.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:gravity="center"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textSize="20sp"
android:text="loading..."/>
</LinearLayout>
下一步编写底部导航的menu
bottomnavigationview.bottomnavigationviewxml
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/fragment_1"
android:icon="@mipmap/ic_launcher"
android:title="T1" />
<item
android:id="@+id/fragment_2"
android:icon="@mipmap/ic_launcher"
android:title="T2"
/>
<item
android:id="@+id/fragment_3"
android:icon="@mipmap/ic_launcher"
android:title="T3"
/>
<item
android:id="@+id/fragment_4"
android:icon="@mipmap/ic_launcher"
android:title="T4"/>
<item
android:id="@+id/fragment_5"
android:icon="@mipmap/ic_launcher"
android:title="T5"/>
</menu>
下一步编写BaseFragment
,抽象出初始化View
的方法和获得布局方法,交由子类实现
BaseFragment
abstract class BaseFragment : Fragment() {
//根布局
private var rootView: View? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
if(rootView == null) {
rootView = inflater.inflate(getLayoutRes(), container, false)
}
initView(rootView)
return rootView
}
protected abstract fun initView(rootView: View?)
protected abstract fun getLayoutRes(): Int
}
下一步编写MyFragment
,根据不同的Position
显示不同的数据
MyFragment
class MyFragment : BaseFragment(){
//定时器,模仿耗时操作
private var con: CountDownTimer? = null
companion object {
fun newInstance(position: Int): MyFragment {
val fragment = MyFragment()
fragment.tabIndex = position
return fragment
}
}
//当前Fragment的位置
var tabIndex = 0
//初始化View
override fun initView(rootView: View?) {
getData()
}
//加载数据
private fun getData() {
con = object : CountDownTimer(2000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
GlobalScope.launch(Dispatchers.Main) {
var color = context?.resources?.getColor(R.color.purple_500)
when (tabIndex) {
1 -> text?.text = "1 get data"
2 -> text?.text = "2 get data"
3 -> text?.text = "3 get data"
4 -> text?.text = "4 get data"
5 -> text?.text = "5 get data"
else -> text?.text = "else get data"
}
if (color != null) {
text?.setTextColor(color)
}
}
}
}
con?.start()
}
//返回布局
override fun getLayoutRes() = R.layout.fragment_first
}
由于ViewPager
是高级控件,必须实现Adapter
MyFragmentPagerAdapter
class MyFragmentPagerAdapter(var fragmentList: List<Fragment>, fm : FragmentManager) : FragmentPagerAdapter(fm) {
companion object {
const val TAG= "MyFragmentPagerAdapter"
}
override fun getCount(): Int = fragmentList.size
override fun getItem(position: Int) = fragmentList.get(position)
}
最后一步实现MainActivity
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//底部导航栏
bottomNavigationView.setOnNavigationItemSelectedListener {
when (it.getItemId()) {
R.id.fragment_1 -> {
viewPager.setCurrentItem(0, true)
true
}
R.id.fragment_2 -> {
viewPager.setCurrentItem(1, true)
true
}
R.id.fragment_3 -> {
viewPager.setCurrentItem(2, true)
true
}
R.id.fragment_4 -> {
viewPager.setCurrentItem(3, true)
true
}
R.id.fragment_5 -> {
viewPager.setCurrentItem(4, true)
true
}
else -> false
}
}
//初始化Fragment
val fragmentList: MutableList<Fragment> = ArrayList<Fragment>()
fragmentList.add(MyFragment.newInstance(1))
fragmentList.add(MyFragment.newInstance(2))
fragmentList.add(MyFragment.newInstance(3))
fragmentList.add(MyFragment.newInstance(4))
fragmentList.add(MyFragment.newInstance(5))
val adapter = MyFragmentPagerAdapter(fragmentList, supportFragmentManager)
viewPager.setAdapter(adapter)
//设置缓存和预加载页面
viewPager.setOffscreenPageLimit(2)
//绑定ViewPager的切换监听器
viewPager.setOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int,
) {
}
override fun onPageSelected(position: Int) {
var itemId = R.id.fragment_1
when (position) {
0 -> itemId = R.id.fragment_1
1 -> itemId = R.id.fragment_2
2 -> itemId = R.id.fragment_3
3 -> itemId = R.id.fragment_4
4 -> itemId = R.id.fragment_5
}
bottomNavigationView.selectedItemId = itemId
}
override fun onPageScrollStateChanged(state: Int) {
}
})
}
}
上述代码运行效果
存在的问题
不知道读者有没有发现问题。
明明每个Fragment
都有定时器,为什么页面1的显示需要2秒的加载,而页面2和页面3直接就显示了呢?
为什么页面四也并没有够两秒的加载时间就显示了数据呢?
分析问题的原因,直接显示或者加载时间不够,只有一个原因就是提前加载了。在企业级开发中每个子页面的网络加载都是很复杂的,如果存在预加载带来的直接问题就是系统的资源占有率过高,极端情况还可能oom
,因此我们要断掉ViewPager
的与预加载。
在解决问题之前,我们需要分析ViewPager
的源码,找到问题的原因。
ViewPager源码分析
本篇文章只分析ViewPager
的缓存和预加载处理
确定分析对象
内存缓存则必定存在集合,这个集合保存的肯定存储的是Fragment
的信息,ViewPager
存在四个集合
private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>(); //1
private List<OnPageChangeListener> mOnPageChangeListeners;//2
private List<OnAdapterChangeListener> mAdapterChangeListeners;//3
private ArrayList<View> mDrawingOrderedChildren;//4
使用排除法
首先排除2,3,查看接口OnPageChangeListener
和OnAdapterChangeListener
明显不能保存Fragment
的信息
在1和4中选,看4的删除和添加时机
看名字也知道4不是缓存,但是为了更严谨,我们分析原因,因为缓存的删除不可能仅仅只有clear
,clear
清楚全部数据明显不符合缓存逻辑。如果不确定可以进入删除和添加的方法中,发现clear
和add
是在一个方法中。
ViewPager#sortChildDrawingOrder
private void sortChildDrawingOrder() {
if (mDrawingOrder != DRAW_ORDER_DEFAULT) {
//重置mDrawingOrderedChildren,只要进入则mDrawingOrderedChildren一定会被重置
if (mDrawingOrderedChildren == null) {
mDrawingOrderedChildren = new ArrayList<View>();
} else {
mDrawingOrderedChildren.clear();
}
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
mDrawingOrderedChildren.add(child);
}
Collections.sort(mDrawingOrderedChildren, sPositionComparator);
}
}
只要命中if
则mDrawingOrderedChildren
一定被重置,在后续再添加,显然缓存不可能如此设计,因此缓存只能是1
private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>(); //1
其保存的信息如下:
static class ItemInfo {
Object object; //ViewPager要缓存的类对象
int position; //当前位置
boolean scrolling; //是否处于滑动状态
float widthFactor; //宽度使用
float offset;
}
确定分析方法
确定完缓存对象,就要寻找真正的缓存处理方法,对其增删的地方进行分析
增有一个方法使用
ViewPager#addNewItem
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;
}
addNewItem
方法被populate()
方法调用
删除有两个方法使用
dataSetChanged()
和populate()
再分析在View
绘制流程中哪个方法被调用,View
的绘制流程中一定会处理缓存
在onMeasure
中发现populate()
是被调用的,而dataSetChanged()
在数据进行改变时调用
因此可以明确的确定populate()
就是缓存处理方法
populate()解析
//传入要切换的View的位置
void populate(int newCurrentItem) {
ItemInfo oldCurInfo = null;
if (mCurItem != newCurrentItem) {
oldCurInfo = infoForPosition(mCurItem);
//保存位置信息
mCurItem = newCurrentItem;
}
//处理绘制排序,不是我们关注的重点
...
//开始绘制
mAdapter.startUpdate(this);
//调用setOffscreenPageLimit()方法设置的缓存数
final int pageLimit = mOffscreenPageLimit;
//本次处理要缓存的右边界
final int startPos = Math.max(0, mCurItem - pageLimit);
//要缓存View的总数
final int N = mAdapter.getCount();
//本次处理要缓存的左边界
final int endPos = Math.min(N - 1, mCurItem + pageLimit);
//经左右边界即可确定ViewPager的缓存规则,假设pageLimit设置为2,总共有5个View,当前在第一个View,则会缓存1,2,3,若在第三个View,则会缓存1,2,3,4,5,若在第五个View则会缓存3,4,5
//此时则存在一个算法问题,使用List,怎么能够去掉其他不要的缓存并保留本次需要的缓存呢,ViewPager使用双指针实现,并左右分治处理,先处理左边界缓存,再处理右边界缓存,用处理左边界举例:一个指针pos指向当前要显示的View的真正的位置,另一个指针itemIndex用于获取当前数组的元素,也可能指向当前缓存数组中的最后一位(若此次切换的页面缓存中不存在),如果此次切换的页面在缓存中则itemIndex指向此页面在List中的下标。使用for循环pos每次减一,直到为0,循环内部存在一个三条分支的if,分支1,当前pos和左边界左比较,如果左边界大,则意味着pos走到了不需要缓存的页面,此时删除此页面,分支2,若pos和当前缓存List中的itemIndex指向的对象中的position相等,则意味着此时此页面还需要缓存,不用删除,itemIndex满足分支二则也需减一,分之3else则添加Item。
if (N != mExpectedAdapterCount) {
//判断N是否合法,不合法则扔出异常
//省略
}
// 下面算法开始
//在缓存中找到当前需要显示页面的Index,若没有找到则为缓存的最后一个元素
int curIndex = -1;
ItemInfo curItem = null;
for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
final ItemInfo ii = mItems.get(curIndex);
if (ii.position >= mCurItem) {
if (ii.position == mCurItem) curItem = ii;
break;
}
}
//缓存中并不存在,则在缓存末尾添加一个ItemInfo
if (curItem == null && N > 0) {
curItem = addNewItem(mCurItem, curIndex);//看下一
}
// Fill 3x the available width or up to the number of offscreen
// pages requested to either side, whichever is larger.
// If we have no current item we have no work to do.
if (curItem != null) {
//左边存在几个
float extraWidthLeft = 0.f;
//循环结束时,等于size,-1是因为要拿到最后一个缓存页面
int itemIndex = curIndex - 1;
//拿到数组中存在的最新页面的左边第一个元素
ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
//当前屏幕宽度
final int clientWidth = getClientWidth();
//基本上永远为1
final float leftWidthNeeded = clientWidth <= 0 ? 0 :
2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
//遍历左边,即使时第10个页面,只缓存2个,那么此循环也会执行10次
//由于pos最开始是大的,因此永远先处理添加,再处理保留页面,再处理删除
for (int pos = mCurItem - 1; pos >= 0; pos--) {
//处理不应该存在的页面
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
//为null 的情况不会出现,这里时健壮性分析
if (ii == null) {
break;
}
//移除,这里有个小坑,因为在刚切换页面时,处于滑动状态,因此并不会立即去除,而是在后期下一次执行populate方法时去除
if (pos == ii.position && !ii.scrolling) {
mItems.remove(itemIndex);
mAdapter.destroyItem(this, pos, ii.object);
if (DEBUG) {
Log.i(TAG, "populate() - destroyItem() with pos: " + pos
+ " view: " + ((View) ii.object));
}
itemIndex--;
curIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
//处理之前缓存过,且这个切换页面仍需保留的页面
} else if (ii != null && pos == ii.position) {
extraWidthLeft += ii.widthFactor;
//itemIndex-- 拿到缓存中前一个位置的item
itemIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
//处理之前没有需要新添加的页面
} else {
ii = addNewItem(pos, itemIndex + 1);
extraWidthLeft += ii.widthFactor;
//curIndex往右边去找最新切换的页面
curIndex++;
//还是拿当前的item
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
}
//右边是对偶的不再分析,执行完左边其实,curIndex指向了新显示页面的下标
float extraWidthRight = curItem.widthFactor;
itemIndex = curIndex + 1;
if (extraWidthRight < 2.f) {
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
final float rightWidthNeeded = clientWidth <= 0 ? 0 :
(float) getPaddingRight() / (float) clientWidth + 2.f;
for (int pos = mCurItem + 1; pos < N; pos++) {
if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
if (ii == null) {
break;
}
if (pos == ii.position && !ii.scrolling) {
mItems.remove(itemIndex);
mAdapter.destroyItem(this, pos, ii.object);
if (DEBUG) {
Log.i(TAG, "populate() - destroyItem() with pos: " + pos
+ " view: " + ((View) ii.object));
}
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {
extraWidthRight += ii.widthFactor;
itemIndex++;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
} else {
ii = addNewItem(pos, itemIndex);
itemIndex++;
extraWidthRight += ii.widthFactor;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
}
}
//计算偏移,和缓存关系不大没必要研究
calculatePageOffsets(curItem, curIndex, oldCurInfo);
//设置要显示的页面
mAdapter.setPrimaryItem(this, mCurItem, curItem.object);//看下2
}
//省略Debug Log
...
//结束更新
mAdapter.finishUpdate(this);//看下三
//处理子View的布局信息,并对绘制排序进行排序,和缓存关系不大没必要研究
...
//处理焦点,和缓存关系不大没必要研究
...
}
上述算法读者一定要自己画画图,跑一跑
1.ViewPager#addNewItem
缓存中添加数据
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;
}
FragmentPagerAdapter#instantiateItem
public Object instantiateItem(@NonNull ViewGroup container, int position) {
//拿到Fragment事务
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
//拿到要缓存的Item的id
final long itemId = getItemId(position);
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
//FragmentManager是否存在此ragment
Fragment fragment = mFragmentManager.findFragmentByTag(name);
//若存在则调用attach,若不存在则add,两个方法的执行的生命周期方法不同
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
//getItem()方法需要自己实现,调用子类Adapter实现的getItem()拿到Fragment
fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
//若和上次显示的Fragment不同则需要特殊处理
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
} else {
//重点方法,设置为fasle,后续的优化需要借助此方法,往下看
fragment.setUserVisibleHint(false);
}
}
return fragment;
}
Fragment#setUserVisibleHint
@Deprecated
public void setUserVisibleHint(boolean isVisibleToUser) {
...
mUserVisibleHint = isVisibleToUser;
...
}
其对应的get
方法如下:
Fragment#getUserVisibleHint
public boolean getUserVisibleHint() {
return mUserVisibleHint;
}
上述添加并没有执行事务的提交也就是说并不会执行Fragment
的生命周期,因此mUserVisibleHint
标志位的设置是在生命周期之前,添加操作mUserVisibleHint
被置为false
,后续优化需要使用此标志位。
2.FragmentPagerAdapter#setPrimaryItem
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
Fragment fragment = (Fragment)object;
//如此次显示的页面和上一个显示的页面不同
if (fragment != mCurrentPrimaryItem) {
//上一个不为null
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
//mBehavior默认为BEHAVIOR_SET_USER_VISIBLE_HINT,因此走else
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
} else {
//上一个页面的mUserVisibleHint置为false
mCurrentPrimaryItem.setUserVisibleHint(false);
}
}
fragment.setMenuVisibility(true);
//mBehavior默认为BEHAVIOR_SET_USER_VISIBLE_HINT,因此走else
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
} else {
//当前页面的mUserVisibleHint置为true
fragment.setUserVisibleHint(true);
}
//切换最新页面
mCurrentPrimaryItem = fragment;
}
}
3.FragmentPagerAdapter#finishUpdate
public void finishUpdate(@NonNull ViewGroup container) {
if (mCurTransaction != null) {
//提交事务,执行生命周期
mCurTransaction.commitNowAllowingStateLoss();
mCurTransaction = null;
}
}
优化的实现思路
基本思路
上述分析完ViewPager
的缓存思路,我们可以确切的知道Fragment
的mUserVisibleHint
标志位只有为当前要显示的页面时才为true
,不显示的页面会置为false
,而且是先于生命周期执行的,因此我们可借助此标志位来控制加载,true
则加载,false
则停止加载或者不加载。
技术思路确定,现在要确定实现思路。实现思路则是标志位在哪判断合适,在Fragment的onCreat()
中判断?还是重写setUserVisibleHint()
判断呢
生命周期
首先要了解ViewPager
在切换Fragment
时,Fragment
的生命周期
上述使用中我们编写了MyFragment,给其加上下面代码
override fun onCreate(savedInstanceState: Bundle?) {
Log.e(javaClass.simpleName + tabIndex, "onCreate")
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Log.e(javaClass.simpleName + tabIndex, "onCreateView")
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onResume() {
Log.e(javaClass.simpleName + tabIndex, "onResume")
super.onResume()
}
override fun onStop() {
Log.e(javaClass.simpleName + tabIndex, "onStop")
super.onStop()
}
override fun onDestroyView() {
Log.e(javaClass.simpleName + tabIndex, "onDestroyView")
super.onDestroyView()
}
override fun onDestroy() {
Log.e(javaClass.simpleName + tabIndex, "onDestroy")
super.onDestroy()
}
app启动时运行如下:
执行1,2,3onCreat
,onCreatView
和onResume
,因为设置的缓存为2,启动时默认为1页面,因此会预加载1,2,3,三个页面
切换到5页面如下:
执行4,5onCreat
,onCreatView
和onResume
,并执行1,2的onStop
和onDestroyView
,如此结果是因为切换到5页面时,缓存里存在的是3,4,5,由于之前3已经再缓存里,因此不会执行3的生命周期,而1,2不在缓存中,需要执行他们的销毁
再切换回1如下:
执行1,2onCreatView
和onResume
,并执行4,5的onStop
和onDestroyView
,如此结果是因为再次切换到1页面时,之前已经创建过因此1,2的onCreat
不会再执行,且此时的缓存应该是1,2,3,3本身就在缓存中,不会执行3的生命周期,而4,5需要去除因此执行销毁
返回Home,如下:
再次进入,如下:
返回home
和进入没有什么好说的
开篇问题解决
此时文章开始问题的答案已经出来了
明明每个Fragment都有定时器,为什么页面1的显示需要2秒的加载,而页面2和页面3直接就显示了呢?
在加载页面1时,2,3页面也在缓存中被预加载。
1的定时器执行完毕,则2,3也一定执行完毕
为什么页面4也并没有够两秒的加载时间就显示了数据呢?
看开篇的gif图,因为笔者是从1->2->3->4的,且设置的缓存为2,当2显示时,4也就添加进了缓存并被预加载,当真正到4的时候,其实定时器已经执行了一部分。
确定标志位的判断位置
回顾一下实现的效果,真正需要显示时才加载,不显示时需要停止加载或者不加载。
onCreat()
方法永远只执行一次,而且也不是我们加载数据的位置,在onCreat()
中写是不符合逻辑的。
onCreatView()
方法是新添加进缓存的页面才会执行,是否加载写在这是否可以呢,写在onCreatView()
存在一种情况,在页面1时,页面2和页面3的onCreatView()
已经执行,若此时切换到页面2或者页面3,onCreatView
不再执行,则加载不出数据。
重写setUserVisibleHint()
进行判断,true
则加载,此时如果onCreatView()
没有加载过,findViewById()
一定返回为null
,导致空指针异常
在哪写都有bug
,因此需要配合实现,我们使用onCreatView()
配合setUserVisibleHint()
实现。
第一版实现
只使用setUserVisibleHint()
实现
所有的懒加载逻辑都放在BaseFragment中编写
BaseFragment
abstract class BaseFragment : Fragment() {
//根布局
private var rootView: View? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
if(rootView == null) {
rootView = inflater.inflate(getLayoutRes(), container, false)
}
initView(rootView)
return rootView
}
protected abstract fun initView(rootView: View?)
protected abstract fun getLayoutRes(): Int
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
//当为true时加载数据,false时停止加载
if (isVisibleToUser) {
dispatchUserVisibleHint(true)
} else {
dispatchUserVisibleHint(false)
}
}
//分发方法
private fun dispatchUserVisibleHint(visibleState: Boolean) {
if (visibleState) {
// 加载数据
onFragmentLoad()
} else {
// 停止一切操作
onFragmentLoadStop()
}
}
// -->>>停止网络数据请求
open fun onFragmentLoadStop() {
}
// -->>>加载网络数据请求
open fun onFragmentLoad() {
}
}
修改MyFragment
MyFragment
class MyFragment : BaseFragment(){
//定时器,模仿耗时操作
private var con: CountDownTimer? = null
//协程返回的job,后期取消使用
private var job: Job? = null
companion object {
fun newInstance(position: Int): MyFragment {
val fragment = MyFragment()
fragment.tabIndex = position
return fragment
}
}
//当前Fragment的位置
var tabIndex = 0
//初始化View
override fun initView(rootView: View?) {
getData()
}
//加载数据
private fun getData() {
con = object : CountDownTimer(2000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
job = GlobalScope.launch(Dispatchers.Main) {
var color = context?.resources?.getColor(R.color.purple_500)
when (tabIndex) {
1 -> text?.text = "1 get data"
2 -> text?.text = "2 get data"
3 -> text?.text = "3 get data"
4 -> text?.text = "4 get data"
5 -> text?.text = "5 get data"
else -> text?.text = "else get data"
}
if (color != null) {
text?.setTextColor(color)
}
}
}
}
con?.start()
}
//暂停加载方法
override fun onFragmentLoadStop() {
super.onFragmentLoadStop()
Log.e(javaClass.simpleName + tabIndex, "onFragmentLoadStop")
con?.cancel()
job?.cancel()
text.setText("暂停加载")
}
//加载方法
override fun onFragmentLoad() {
super.onFragmentLoad()
Log.e(javaClass.simpleName + tabIndex, "onFragmentLoad")
getData()
}
//返回布局
override fun getLayoutRes() = R.layout.fragment_first
override fun onCreate(savedInstanceState: Bundle?) {
Log.e(javaClass.simpleName + tabIndex, "onCreate")
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
Log.e(javaClass.simpleName + tabIndex, "onCreateView")
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onResume() {
Log.e(javaClass.simpleName + tabIndex, "onResume")
super.onResume()
}
override fun onStop() {
Log.e(javaClass.simpleName + tabIndex, "onStop")
super.onStop()
}
override fun onDestroyView() {
Log.e(javaClass.simpleName + tabIndex, "onDestroyView")
super.onDestroyView()
}
override fun onDestroy() {
Log.e(javaClass.simpleName + tabIndex, "onDestroy")
super.onDestroy()
}
}
上述代码运行直接崩溃,因为在setUserVisibleHint()
中去执行更新ui
操作时,onCreatView
还没有返回布局。因此我们需要判断是否执行了onCreatView()
后续版本中MyFragment
变动不大,主要是修改BaseFragment
第二版实现
在第一版的基础上加入标志位判断,但是仅仅加入标志位,还存在问题,比如第一次加载进入第一个页面,执行setUserVisibleHint()
方法确实不会崩溃,但是由于onCreatView()
未执行,标志位还是false
,onFragmentLoad()
照样不会执行,因此需要在onCreatView()
手动调用分发方法。
BaseFragment
abstract class BaseFragment : Fragment() {
//根布局
private var rootView: View? = null
//是否执行过onCreateView()
private var isViewCreated = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
if(rootView == null) {
rootView = inflater.inflate(getLayoutRes(), container, false)
}
initView(rootView)
isViewCreated = true
if (userVisibleHint) {
userVisibleHint = true
}
return rootView
}
protected abstract fun initView(rootView: View?)
protected abstract fun getLayoutRes(): Int
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (isViewCreated) {
if (isVisibleToUser) {
dispatchUserVisibleHint(true)
} else {
dispatchUserVisibleHint(false)
}
}
}
private fun dispatchUserVisibleHint(visibleState: Boolean) {
if (visibleState) {
// 加载数据
onFragmentLoad()
} else {
// 停止一切操作
onFragmentLoadStop()
}
}
// -->>>停止网络数据请求
open fun onFragmentLoadStop() {
}
// -->>>加载网络数据请求
open fun onFragmentLoad() {
}
}
此时会出现一种崩溃情况则是,当从第一页切换到第五页,再切换到第四页时,崩溃
崩溃原因是,加载第一页时第二页和第三页被缓存,执行了onCreateView()
,标志位isViewCreated
为true
,当跳转到第五页时,第一页,第二页,第三页执行onDestroyView()
,此时布局没了,当切换到第四页时,第二页和第三页进入缓存,执行第三页和第二页的setUserVisibleHint()
发现isViewCreated
仍为true
且传入参数为false
,执行onFragmentLoadStop()
方法,MyFragment
中的onFragmentLoadStop()
更新了UI
,又因为之前执行了onDestroyView()
,控件已经拿不到了,则出现空指针异常。
第三版实现
解决上述BUG
,需要正确对可见和不可见进行定义
可见是从不可见到可见才算可见
不可见是可见到不可见才算不可见
只有状态发生改变才是整整的改变
因此在真正可见和不可见的时候更新UI
就不会导致空指针异常
加入一个标志位保存上一次的状态,修改如下:
abstract class BaseFragment : Fragment() {
//根布局
private var rootView: View? = null
//是否执行过onCreateView()
private var isViewCreated = false
//上次是否可见
private var isVisibleStateUP = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
if(rootView == null) {
rootView = inflater.inflate(getLayoutRes(), container, false)
}
initView(rootView)
isViewCreated = true
if (userVisibleHint) {
userVisibleHint = true
}
return rootView
}
protected abstract fun initView(rootView: View?)
protected abstract fun getLayoutRes(): Int
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (isViewCreated) {
// 记录上一次可见的状态: && isVisibleStateUP
if (isVisibleToUser && !isVisibleStateUP) {
dispatchUserVisibleHint(true)
} else if (!isVisibleToUser && isVisibleStateUP) {
dispatchUserVisibleHint(false)
}
}
}
private fun dispatchUserVisibleHint(visibleState: Boolean) {
// 记录上一次可见的状态 实时更新状态
isVisibleStateUP = visibleState
if (visibleState) {
// 加载数据
onFragmentLoad()
} else {
// 停止一切操作
onFragmentLoadStop()
}
}
// -->>>停止网络数据请求
open fun onFragmentLoadStop() {
}
// -->>>加载网络数据请求
open fun onFragmentLoad() {
}
}
上面实现还是存在问题的,如果我们跳转其他Activity
,那么当前Fragment
应该停止加载,返回的时候应该继续加载,因此需要在onResume()
和onPause()
中调用分发方法。出现问题的原因很简单,跳转Activity
时并不会执行populate()
,则不会调用setUserVisibleHint()
因此停止和加载方法失效。
第四版实现
解决上述BUG
在onResume()
和onPause()
中分发相应的事件即可
BaseFragment
中添加下面代码
override fun onResume() {
super.onResume()
//不可见 到 可见 变化过程 说明可见
if (userVisibleHint && !isVisibleStateUP) {
dispatchUserVisibleHint(true)
}
}
override fun onPause() {
super.onPause()
//可见 到 不可见 变化过程 说明 不可见
if (userVisibleHint && isVisibleStateUP) {
dispatchUserVisibleHint(false)
}
}
此时还是存在问题的,若有Fragment
内部嵌套ViewPager
则会出错。一般来说Fragment
嵌套的ViewPager
会在initView()
方法中进行初始化,执行onCreateView()
则initView()
必执行,当initView()
执行出现的问题就是,内部嵌套的第一页的Fragment
被加载。
第四版的实现效果如下:
第五版实现
解决上述问题,只需要让子Fragment
与父Fragment
建立联系即可
修改分发方法如下:
BaseFragment#dispatchUserVisibleHint
private fun dispatchUserVisibleHint(visibleState: Boolean) {
// TODO 记录上一次可见的状态 实时更新状态
isVisibleStateUP = visibleState
// 如果父不可见则直接return
if (visibleState && isParentInvisible()) {
return
}
if (visibleState) {
// 加载数据
onFragmentLoad()
} else {
// 停止一切操作
onFragmentLoadStop()
}
}
判断父Fragment
的方法如下:
BaseFragment#isParentInvisible
private fun isParentInvisible(): Boolean {
//拿到父Fragment
val parentFragment = parentFragment
if (parentFragment is BaseFragment) {
val fragment= parentFragment
//返回是否可见
return !fragment.isVisibleStateUP
}
return false
}
上述改动虽然解决了加载嵌套页面第一页的BUG
,但是还存在问题,假设页面2存在嵌套ViewPager
,当从页面1切换到页面2时,页面2的ViewPager
并不会执行populate()
,不执行populate()
意味着不执行setUserVisibleHint()
,导致嵌套ViewPager
中的Fragment
加载不出来,因此我们需要手动进行分发,分发方法如下:
protected fun dispatchChildVisibleState(state: Boolean) {
val fragmentManager = childFragmentManager
val fragments: List<Fragment> = fragmentManager.getFragments()
if (fragments != null) {
for (fragment in fragments) { // 循环遍历 嵌套里面的 子 Fragment 来分发事件操作
if (fragment is BaseFragment && !fragment.isHidden && fragment.userVisibleHint) {
(fragment as BaseFragment).dispatchUserVisibleHint(state)
}
}
}
}
在dispatchUserVisibleHint
进行调用:
private fun dispatchUserVisibleHint(visibleState: Boolean) {
// TODO 记录上一次可见的状态 实时更新状态
isVisibleStateUP = visibleState
// 如果父不可见则直接return
if (visibleState && isParentInvisible()) {
return
}
if (visibleState) {
// 加载数据
onFragmentLoad()
//可见时需要对子页面进行手动加载
dispatchChildVisibleState(true)
} else {
// 停止一切操作
onFragmentLoadStop()
//当不可见时需要手动停止子页面的加载
dispatchChildVisibleState(false)
}
}
完整版的BaseFragment
如下:
BaseFragment
abstract class BaseFragment : Fragment() {
//根布局
private var rootView: View? = null
//是否执行过onCreateView()
private var isViewCreated = false
//上次是否可见
private var isVisibleStateUP = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
if(rootView == null) {
rootView = inflater.inflate(getLayoutRes(), container, false)
}
initView(rootView)
isViewCreated = true
if (userVisibleHint) {
userVisibleHint = true
}
return rootView
}
protected abstract fun initView(rootView: View?)
protected abstract fun getLayoutRes(): Int
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (isViewCreated) {
// TODO 记录上一次可见的状态: && isVisibleStateUP
if (isVisibleToUser && !isVisibleStateUP) {
dispatchUserVisibleHint(true)
} else if (!isVisibleToUser && isVisibleStateUP) {
dispatchUserVisibleHint(false)
}
}
}
private fun dispatchUserVisibleHint(visibleState: Boolean) {
// TODO 记录上一次可见的状态 实时更新状态
isVisibleStateUP = visibleState
// 如果父不可见则直接return
if (visibleState && isParentInvisible()) {
return
}
if (visibleState) {
// 加载数据
onFragmentLoad()
//可见时需要对子页面进行手动加载
dispatchChildVisibleState(true)
} else {
// 停止一切操作
onFragmentLoadStop()
//当不可见时需要手动停止子页面的加载
dispatchChildVisibleState(false)
}
}
//判断 父控件 是否可见
private fun isParentInvisible(): Boolean {
//拿到父Fragment
val parentFragment = parentFragment
if (parentFragment is BaseFragment) {
val fragment= parentFragment
//返回是否可见
return !fragment.isVisibleStateUP
}
return false
}
//分发子View事件
protected fun dispatchChildVisibleState(state: Boolean) {
val fragmentManager = childFragmentManager
val fragments: List<Fragment> = fragmentManager.getFragments()
if (fragments != null) {
for (fragment in fragments) { // 循环遍历 嵌套里面的 子 Fragment 来分发事件操作
if (fragment is BaseFragment && !fragment.isHidden && fragment.userVisibleHint) {
(fragment as BaseFragment).dispatchUserVisibleHint(state)
}
}
}
}
// -->>>停止网络数据请求
open fun onFragmentLoadStop() {
}
// -->>>加载网络数据请求
open fun onFragmentLoad() {
}
override fun onResume() {
super.onResume()
//不可见 到 可见 变化过程 说明可见
if (userVisibleHint && !isVisibleStateUP) {
dispatchUserVisibleHint(true)
}
}
override fun onPause() {
super.onPause()
//可见 到 不可见 变化过程 说明 不可见
if (userVisibleHint && isVisibleStateUP) {
dispatchUserVisibleHint(false)
}
}
}
第五版实现就是最终版本的Fragment
,完整的实现了懒加载。
ViewPage1和ViewPage2的区别
上述优化都是基于ViewPager1
在Viewpager2
中setUserVisibleHint()
和getUserVisibleHint()
已经弃用
ViewPage2
使用Jetpack
实现,Fragment
的生命周期通过Lifecycles
管理,setUserVisibleHint()
和getUserVisibleHint()
使用setMaxLifecycle
代替
ViewPager1
使用了一个List
进行缓存管理,ViewPager2
使用RecyclerView
的缓存机制
笔者还未学习Jetpack
,等笔者修炼修炼再对ViewPager2
进行研究
总结
setUserVisibleHint()
先于Fragment
生命周期执行,因此给我们提供了懒加载思路,后期的优化不断改进即可。
✨ 原 创 不 易 , 还 希 望 各 位 大 佬 支 持 一 下 \textcolor{blue}{原创不易,还希望各位大佬支持一下} 原创不易,还希望各位大佬支持一下
👍 点 赞 , 你 的 认 可 是 我 创 作 的 动 力 ! \textcolor{green}{点赞,你的认可是我创作的动力!} 点赞,你的认可是我创作的动力!
⭐️ 收 藏 , 你 的 青 睐 是 我 努 力 的 方 向 ! \textcolor{green}{收藏,你的青睐是我努力的方向!} 收藏,你的青睐是我努力的方向!
✏️ 评 论 , 你 的 意 见 是 我 进 步 的 财 富 ! \textcolor{green}{评论,你的意见是我进步的财富!} 评论,你的意见是我进步的财富!