ViewPager1中Fragment的懒加载实现

请添加图片描述

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,查看接口OnPageChangeListenerOnAdapterChangeListener明显不能保存Fragment的信息

在1和4中选,看4的删除和添加时机

看名字也知道4不是缓存,但是为了更严谨,我们分析原因,因为缓存的删除不可能仅仅只有clearclear清楚全部数据明显不符合缓存逻辑。如果不确定可以进入删除和添加的方法中,发现clearadd是在一个方法中。

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);
    }
}

只要命中ifmDrawingOrderedChildren一定被重置,在后续再添加,显然缓存不可能如此设计,因此缓存只能是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的缓存思路,我们可以确切的知道FragmentmUserVisibleHint标志位只有为当前要显示的页面时才为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,3onCreatonCreatViewonResume,因为设置的缓存为2,启动时默认为1页面,因此会预加载1,2,3,三个页面

切换到5页面如下:

执行4,5onCreatonCreatViewonResume,并执行1,2的onStoponDestroyView,如此结果是因为切换到5页面时,缓存里存在的是3,4,5,由于之前3已经再缓存里,因此不会执行3的生命周期,而1,2不在缓存中,需要执行他们的销毁

再切换回1如下:

执行1,2onCreatViewonResume,并执行4,5的onStoponDestroyView,如此结果是因为再次切换到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()未执行,标志位还是falseonFragmentLoad()照样不会执行,因此需要在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(),标志位isViewCreatedtrue,当跳转到第五页时,第一页,第二页,第三页执行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()因此停止和加载方法失效。

第四版实现

解决上述BUGonResume()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

Viewpager2setUserVisibleHint()getUserVisibleHint()已经弃用

ViewPage2使用Jetpack实现,Fragment的生命周期通过Lifecycles管理,setUserVisibleHint()getUserVisibleHint()使用setMaxLifecycle代替

ViewPager1使用了一个List进行缓存管理,ViewPager2使用RecyclerView的缓存机制

笔者还未学习Jetpack,等笔者修炼修炼再对ViewPager2进行研究

总结

setUserVisibleHint()先于Fragment生命周期执行,因此给我们提供了懒加载思路,后期的优化不断改进即可。

原 创 不 易 , 还 希 望 各 位 大 佬 支 持 一 下 \textcolor{blue}{原创不易,还希望各位大佬支持一下}

👍 点 赞 , 你 的 认 可 是 我 创 作 的 动 力 ! \textcolor{green}{点赞,你的认可是我创作的动力!}

⭐️ 收 藏 , 你 的 青 睐 是 我 努 力 的 方 向 ! \textcolor{green}{收藏,你的青睐是我努力的方向!}

✏️ 评 论 , 你 的 意 见 是 我 进 步 的 财 富 ! \textcolor{green}{评论,你的意见是我进步的财富!}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值