0
点赞
收藏
分享

微信扫一扫

手把手讲解 ViewPager懒加载

前言

学到老活到老,路漫漫其修远兮。与众君共勉 !


正文大纲

先给出github Demo: https://github.com/18598925736/ViewPagerKeng


正文


一 、ViewPager+Fragment"诡异"的缓存特性


二 、"诡异"特性带来的后果

看来ViewPager + Fragment的默认机制,很有可能造成app里的一个大坑,那怎么办?

只能自己设计一套懒加载机制

所谓懒加载,就是:


三 、必要的基础知识

ActivityFragment的生命周期函数图,我就不贴出来了,大家可以百度。以下几点,是本人写demo做试验所得。

1) 关于Fragment在结合ViewPager的情况(包括内嵌ViewPager+Fragment)下的生命周期函数 执行流程的几个重要结论:

2) 滑动ViewPager,所有的Fragment都只会有一次onCreate,然而,View会经历销毁/重建

onCreateView-onViewCreated-onStart-onResume -----> onPause-onStop-onDestroyView

3)Fragment其实还有两个与 它生命周期无关的 函数,这两个函数 可以控制 Fragment的当前可见状态值 ,但是, 他只是一个特征值而已,并不能证明它就是可见的或者不可见, 他们分别是:

这两个函数,不能确保在生命周期的哪个阶段调用,但是可以根据他们,来配合生命周期函数,判定当前fragment是不是可见.


四 、ViewPager 相关源码索引

终于要进入源码了。

之前提到过,ViewPager 是自带缓存机制的,并且默认缓存数量是1个。

那么有几个问题:

进入ViewPager.java (SDK 27)源代码一起来探索一下:

setOffscreenPageLimit() 作为入口(为什么是它? 因为它是设置缓存数量的函数

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;// 强制设定为1
        }
        if (limit != mOffscreenPageLimit) {//如果缓存页数和当前值不一致
            mOffscreenPageLimit = limit;//那么更新当前值
            populate();//并且执行缓存方法
        }
    }

上述注释很明确,如果入参是0,那么强行改为1,也就是说ViewPager不允许无缓存的情况。而且,在缓存数量更新的时候,要执行缓存方法polulate()

那么polulate()都做了什么?

void populate(int newCurrentItem) {
        ItemInfo oldCurInfo = null;
        int focusDirection = View.FOCUS_FORWARD;
        if (mCurItem != newCurrentItem) {
            focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
            oldCurInfo = infoForPosition(mCurItem);
            mCurItem = newCurrentItem;
        }

        if (mAdapter == null) { //adapter是空,就不进行后面的操作了
            sortChildDrawingOrder();
            return;
        }

        // Bail now if we are waiting to populate.  This is to hold off
        // on creating views from the time the user releases their finger to
        // fling to a new position until we have finished the scroll to
        // that position, avoiding glitches from happening at that point.
        if (mPopulatePending) {
            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
            sortChildDrawingOrder();
            return;
        }

        // Also, don't populate until we are attached to a window.  This is to
        // avoid trying to populate before we have restored our view hierarchy
        // state and conflicting with what is restored.
        if (getWindowToken() == null) {
            return;
        }

        mAdapter.startUpdate(this);//当一个改变发生在已经显示出来的页面时执行(貌似和缓存无关)

        final int pageLimit = mOffscreenPageLimit;//当前缓存数
        final int startPos = Math.max(0, mCurItem - pageLimit);//用当前页编号和缓存数的计算缓存开始的位置 
        final int N = mAdapter.getCount();//获得 adapter的子内容数量
        final int endPos = Math.min(N-1, mCurItem + pageLimit);//用当前页编号和缓存数的计算缓存结束的位置 

        if (N != mExpectedAdapterCount) {//如果adapter的内容数量和期望值不同,就抛出异常
            String resName;
            try {
                resName = getResources().getResourceName(getId());
            } catch (Resources.NotFoundException e) {
                resName = Integer.toHexString(getId());
            }
            throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +
                    " contents without calling PagerAdapter#notifyDataSetChanged!" +
                    " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +
                    " Pager id: " + resName +
                    " Pager class: " + getClass() +
                    " Problematic adapter: " + mAdapter.getClass());
        }

        // Locate the currently focused item or add it if needed.
        // 本地化当前聚焦的item 或者 有必要的话将它加到list中去
        int curIndex = -1;
        ItemInfo curItem = null;//你就是当前正在显示的item
        for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
            //这里在遍历一个mItems,它是ArrayList<ItemInfo> mItems
            final ItemInfo ii = mItems.get(curIndex);// 获得当前位置item
            if (ii.position >= mCurItem) {// 
                if (ii.position == mCurItem) curItem = ii;
                //讲道理,上面两行代码我没看懂,先判定>= ,又判定==,最后把当前缓存的这个,赋值给curItem
                break;
            }
        }

        if (curItem == null && N > 0) {// 如果curItem经历了上面的遍历,还是null,并且adapter的内容数量大于0
            curItem = addNewItem(mCurItem, curIndex);//那么就构建一个ItemInfo对象,并且赋值给curItem
        }

        // 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.
        // 这段注释的意思是,填充三倍可见宽度 或者一直到 屏幕外页面的数量需要的宽度,无论它多大.
        // 如果我们没有当前item,就不用做任何事情
        if (curItem != null) {
            float extraWidthLeft = 0.f;
            int itemIndex = curIndex - 1;//??
            ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;//itemIndex已经是0了,所以,这里就是获取当前位置的item
            final int clientWidth = getPaddedWidth();
            final float leftWidthNeeded = clientWidth <= 0 ? 0 :
                    2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
            for (int pos = mCurItem - 1; pos >= 0; pos--) {//从当前显示的item倒过来往前遍历
                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                    if (ii == null) {
                        break;
                    }
                    if (pos == ii.position && !ii.scrolling) {//如果遍历到了缓存item所在的位置,并且缓存的这个item没有处于滑动状态
                        mItems.remove(itemIndex);//就从缓存list中移除它
                        mAdapter.destroyItem(this, pos, ii.object);//并且 adapter执行destroyItem销毁item
                        if (DEBUG) {
                            Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
                                    " view: " + ii.object);
                        }
                        itemIndex--;
                        curIndex--;
                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                    }
                } else if (ii != null && pos == ii.position) {//如果遍历到了 当前显示的item
                    extraWidthLeft += ii.widthFactor;
                    itemIndex--;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                } else {//其他情况 
                    ii = addNewItem(pos, itemIndex + 1);// 加入缓存
                    extraWidthLeft += ii.widthFactor;
                    curIndex++;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                }
            }

            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);//从缓存中移除掉当前item,因为正在显示中,所以无需缓存
                            mAdapter.destroyItem(this, pos, ii.object);
                            if (DEBUG) {
                                Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
                                        " 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);//// Fix up offsets for later layout. 为了后面的布局,修复偏移量,没有涉及到缓存list的add和remove
        }

        if (DEBUG) {
            Log.i(TAG, "Current page list:");
            for (int i=0; i<mItems.size(); i++) {
                Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
            }
        }

上面的代码中提到了 addNewItem() ,它就是往缓存ArrayList<ItemInfo> mItems里面增加ItemInfo的 核心方法

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

看了这个方法,得出结论:

从上面的结论,我们总结一下,ViewPager缓存的,是PageAdapter mAdapter里面 instantiateItem 创建出来的object, 我们今天重点应该关注的是和Fragment有关的FragmentPagerAdapter,所以,我们看看它的instantiateItem到底返回了一个神马东西:

public Object instantiateItem(ViewGroup container, int position) {
        //...省略非重点代码 
        return fragment;
    }

看到了吧? return fragment !

所以,得出最终结论


五 、ViewPager+Fragment懒加载机制的设计思路

既然ViewPager已经这么明确了,他就是要缓存至少一个Fragment,我们不太好去直接改源码,那怎么去 实现 懒加载呢?没办法了,曲线救国吧,改不了 ViewPager,那就 在Fragment的生命周期上做文章。

Fragment的生命周期也不太好去动刀,但是我们可以增加另外的方法,来结合生命周期,以及 上文提及的无关生命周期的两个函数 setUserVisibleHint / onHiddenChanged.

原本的Fragment生命周期函数不可信了,那么首先我们继承Fragment,增加3个我们自己的方法, 以及3bool标志位

public abstract class BaseLazyLoadingFragment extends Fragment {    
    //省略无关代码...
    private boolean isViewCreated = false;//View是否已经被创建出来

    private boolean isFirstVisible = true;//当前Fragment是否是首次可见

    private boolean currentVisibleState = false;//当前真正的可见状态
    /**
     * 当第一次可见的时候(此方法,在View的一次生命周期中只执行一次)
     * 如果Fragment经历了onDestroyView,那么整个方法会再次执行
     * 重写此方法时,对Fragment全局变量进行 初始化
     * 具体的参照github demo
     */
    protected void onFragmentFirstVisible() {
        Log.d(getCustomMethodTag(), "第一次可见,进行当前Fragment初始化操作");
    }

    /**
     * 当fragment变成可见的时候(可能会多次)
     */
    protected void onFragmentResume() {
        Log.d(getCustomMethodTag(), "onFragmentResume 执行网络请求以及,UI操作");
    }

    /**
     * 当fragment变成不可见的时候(可能会多次)
     */
    protected void onFragmentPause() {
        Log.d(getCustomMethodTag(), "onFragmentPause 中断网络请求,UI操作");
    }   
}

我们设计了3个自定义的方法,那么这3个方法在什么情况下执行呢?

定义一个 visible状态分发的方法:

void dispatchVisibleState(boolean isVisible) {
        //为了兼容内嵌ViewPager的情况,分发时,还要判断父Fragment是不是可见
        if (isVisible && isParentInvisible()) {//如果当前可见,但是父容器不可见,那么也不必分发
            return;
        }
        if (isVisible == currentVisibleState) return;//如果目标值,和当前值相同,那就别费劲了
        currentVisibleState = isVisible;//更新状态值
        if (isVisible) {//如果可见
            //那就区分是第一次可见,还是非第一次可见
            if (isFirstVisible) {
                isFirstVisible = false;
                onFragmentFirstVisible();
            }
            onFragmentResume();
            dispatchChildVisibilityState(true);
        } else {
            onFragmentPause();
            dispatchChildVisibilityState(false);
        }
    }

接着Fragment原有的生命周期函数内( 主要是OnCreateViewonResumeonPause),调用此方法

public abstract class BaseLazyLoadingFragment extends Fragment {

    //省略无关代码...
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        Log.d(getLifeCycleTag(), "onAttach");
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        Log.d(getLifeCycleTag(), "onCreate");
        super.onCreate(savedInstanceState);
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        mRoot = inflater.inflate(getLayoutId(), container, false);
        Log.d(getLifeCycleTag(), "onCreateView");
        initView(mRoot);
        //在View创建完毕之后,isViewCreate 要变为true
        isViewCreated = true;
        if (!isHidden() && getUserVisibleHint())
            dispatchVisibleState(true);
        return mRoot;
    }

    @Override
    public void onDestroyView() {//相对应的,当View被销毁的时候,isViewCreated要变为false
        super.onDestroyView();
        Log.d(getLifeCycleTag(), "onDestroyView");
        reset();
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        Log.d(getLifeCycleTag(), "onViewCreated");
    }

    @Override
    public void onStart() {
        super.onStart();
        Log.d(getLifeCycleTag(), "onStart");
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.d(getLifeCycleTag(), "onResume");
       
        if (!isFirstVisible) {
            if (!isHidden() && !currentVisibleState && getUserVisibleHint())
                dispatchVisibleState(true);
        }

    }

    @Override
    public void onPause() {
        super.onPause();
        Log.d(getLifeCycleTag(), "onPause");
        if (currentVisibleState && getUserVisibleHint()) {
            dispatchVisibleState(false);
        }
    }

    @Override
    public void onStop() {
        super.onStop();
        Log.d(getLifeCycleTag(), "onStop");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(getLifeCycleTag(), "onDestroy");
    }

    @Override
    public void onDetach() {
        super.onDetach();
        Log.d(getLifeCycleTag(), "onDetach");
    }

    //省略无关代码...
}

然而,能够表示当前Fragment是否可见的,并不止有生命周期函数,还有2个和生命周期无关的函数(setUserVisibleHint,onHiddenChanged),在其中也要调用dispatchVisibleState

    
    /**
     * 此方法和生命周期无关,由外部调用,只是作为一个可见不可见的参考
     */
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        Log.d(getLogTag(), "setUserVisibleHint:" + isVisibleToUser);

        //因为只有在Fragment的View已经被创建的前提下,UI处理才有意义,所以
        if (isViewCreated) {
            //为了逻辑严谨,必须当目前状态值和目标相异的时候,才去执行UI可见分发
            if (currentVisibleState && !isVisibleToUser) {
                dispatchVisibleState(false);
            } else if (!currentVisibleState && isVisibleToUser) {
                dispatchVisibleState(true);
            }
        }

    }

    /**
     * 在Fragment被hide/show的时候被调用
     * @param hidden
     */
    @Override
    public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        if (hidden) {
            dispatchVisibleState(false);
        } else {
            dispatchVisibleState(true);
        }
    }

最后,考虑到ViewPager+Fragment(ViewPager+Fragment)嵌套的问题:

设计一个 dispatchChildVisibilityState 方法,来控制 内嵌的Fragment的可见状态 :

private void dispatchChildVisibilityState(boolean isVisible) {
        FragmentManager fragmentManager = getChildFragmentManager();
        List<Fragment> list = fragmentManager.getFragments();
        if (list != null) {
            for (Fragment fg : list) {//遍历子
                if (fg instanceof BaseLazyLoadingFragment
                        && !fg.isHidden() && fg.getUserVisibleHint()) {
                    ((BaseLazyLoadingFragment) fg).dispatchVisibleState(isVisible);
                }
            }
        }
    }

以及isParentInvisible方法来判定内嵌Fragment的父Fragment是否可见:

private boolean isParentInvisible() {
        Fragment parent = getParentFragment();
        Log.d(getLogTag(), "getParentFragment:" + parent + "");
        if (parent instanceof BaseLazyLoadingFragment) {
            BaseLazyLoadingFragment lz = (BaseLazyLoadingFragment) parent;
            return !lz.currentVisibleState;
        }
        return false;// 默认可见
    }

六 、案例演示(观察日志)

最简单的情况,单个ViewPager内滑动时

ViewPager所在Activity 发生跳转,又跳回来

ViewPager嵌套 + 所在Activity发生跳转,又跳回来

其实最后还有一种操作 就是 FragmentManagershow hide,结果也是一样,没有多余的操作,时间原因,不写在demo里面了.


七 、总结 程序开发者的修炼之路


八 、鸣谢

举报

相关推荐

0 条评论