美文网首页四大组件安卓
ViewPager+Fragment预加载和懒加载分析

ViewPager+Fragment预加载和懒加载分析

作者: ag4kd | 来源:发表于2019-08-01 19:02 被阅读0次

    1 什么是fragment的预加载和懒加载?

    预加载:viewpager显示当前fragment的时候,viewpager还会去预加载其他fragment的数据。预加载的Fragment

    懒加载:加载的内容是否需要优化,网络数据的优化。即懒加载的是数据。

    2 为什么要进行懒加载?

    fragment的懒加载是指Fragment与ViewPager结合使用的使用,用到的一种优化方案。

    因为缓存的存在,我觉得应该是因为预加载的存在,之所以要懒加载,就是因为预加载。这里的预加载指的是预加载ViewPager对Fragment的预加载,懒加载是指Fragment对数据的懒加载

    viewpager显示当前fragment的时候,viewpager还会去预加载其他fragment的数据。进而导致界面卡顿,影响用户体验。

    界面卡顿优化:

    1、检查界面是否有过多的渲染;

    2、加载的内容是否需要优化,网络数据的优化。此处可以使用懒加载来解决。

    3 ViewPager预加载分析

    实现fragment懒加载的原理,首先要了解ViewPager预加载Fragment的原理,在viewpager预加载fragment的基础之上,实现fragment的数据的懒加载。

    3.1 设置预加载个数的函数

    setOffscreenPageLimit(int limit)设置视图层次结构中处于空闲状态时,应该保留在当前页面两侧的页面数量。超过此限制的页面将在需要时从适配器重新创建。这是一个优化。

    如果预先知道需要支持的页面数量,或者在页面上设置了延迟加载机制,那么调整此设置将有利于页面动画和交互的流畅性。

    如果您有少量的页面(3-4),您可以同时保持活动状态,那么在布局中为新创建的视图子树来回切换用户页面所花费的时间就会更少。您应该将这个限制保持在较低的水平,特别是如果您的页面具有复杂的布局。此设置默认为1。

    ViewPager.java

    public void setOffscreenPageLimit(int limit) {
        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();
        }
    }
    
    
    private static final int DEFAULT_OFFSCREEN_PAGES = 1;
    private int mOffscreenPageLimit = 1;
    
    

    从源码中可以发现,limit最小值为默认值1.设置缓存个数示例:

    ViewPager.java

    示例1:

    mViewPager.setAdapter(new TestPagerAdapter(getSupportFragmentManager(),mFragments));
    mViewPager.setOffscreenPageLimit(3);
    
    

    示例2:

    mViewPager.setOffscreenPageLimit(3);
    mViewPager.setAdapter(new TestPagerAdapter(getSupportFragmentManager(),mFragments));
    
    

    分析一下populate()函数:

    void populate() {
        this.populate(this.mCurItem);
    }
    
    

    从源码中可以看到,该函数直接调用void populate(int newCurrentItem){}方法。通过调试发现上述两个示例均未触发void populate(int newCurrentItem){}中的核心代码。

    针对示例1的调试mViewPager.setOffscreenPageLimit(3);

    void populate(int newCurrentItem) {
        ItemInfo oldCurInfo = null;
        //newCurrentItem=0,this.mCurItem=0
        if (mCurItem != newCurrentItem) {
            oldCurInfo = infoForPosition(mCurItem);
            mCurItem = newCurrentItem;
        }
        // this.mAdapter!=null
        if (mAdapter == null) {
            sortChildDrawingOrder();
            return;
        }
    
        // this.mPopulatePending=false
        if (mPopulatePending) {
            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
            sortChildDrawingOrder();
            return;
        }
    
        // this.getWindowToken() == null
        if (getWindowToken() == null) {
            return;
        }
         //省略后面代码
    }
    
    

    针对示例2的调试mViewPager.setOffscreenPageLimit(3);

    void populate(int newCurrentItem) {
        ItemInfo oldCurInfo = null;
        //newCurrentItem=0,this.mCurItem=0
        if (mCurItem != newCurrentItem) {
            oldCurInfo = infoForPosition(mCurItem);
            mCurItem = newCurrentItem;
        }
    
            // this.mAdapter == null 
        if (mAdapter == null) {
            sortChildDrawingOrder();//会走这一步
            return;
        }
        //省略此部分代码 
    }
    
    

    看一下sortChildDrawingOrder()的源码实现:

    private void sortChildDrawingOrder() {
        if (this.mDrawingOrder != 0) {
        //省略此部分代码
        }
    }
    
    

    此时的this.mDrawingOrder = 0,所以直接返回函数调用处,接着退出void populate(int newCurrentItem)函数,返回到populate()里面的调用处,进而回到setOffscreenPageLimit(int limit)里面.

    实例1、实例2的整个过程中没有触发任何对Item的操作。所以说,设置适配器和设置预加载的数量的先后顺序对缓存的添加没有影响。一种数学关系:加载的Fragment的数量等于预加载的Fragment数量1

    3.2 设置适配器

    /**
     * Set a PagerAdapter that will supply views for this pager as needed.
     *
     * @param adapter Adapter to use
     */
    public void setAdapter(PagerAdapter adapter) {
        if (mAdapter != null) {
            mAdapter.setViewPagerObserver(null);
            mAdapter.startUpdate(this);
            for (int i = 0; i < mItems.size(); i++) {
                final ItemInfo ii = mItems.get(i);
                mAdapter.destroyItem(this, ii.position, ii.object);
            }
            mAdapter.finishUpdate(this);
            mItems.clear();
            removeNonDecorViews();
            mCurItem = 0;
            scrollTo(0, 0);
        }
    
        final PagerAdapter oldAdapter = mAdapter;
        mAdapter = adapter;
        mExpectedAdapterCount = 0;
    
        if (mAdapter != null) {
            if (mObserver == null) {
                mObserver = new PagerObserver();
            }
            mAdapter.setViewPagerObserver(mObserver);
            mPopulatePending = false;
            final boolean wasFirstLayout = mFirstLayout;
            mFirstLayout = true;
            mExpectedAdapterCount = mAdapter.getCount();
            if (mRestoredCurItem >= 0) {
                mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
                setCurrentItemInternal(mRestoredCurItem, false, true);
                mRestoredCurItem = -1;
                mRestoredAdapterState = null;
                mRestoredClassLoader = null;
            } else if (!wasFirstLayout) {
                populate();
            } else {
                requestLayout();
            }
        }
    
        // Dispatch the change to any listeners
        if (mAdapterChangeListeners != null && !mAdapterChangeListeners.isEmpty()) {
            for (int i = 0, count = mAdapterChangeListeners.size(); i < count; i++) {
                mAdapterChangeListeners.get(i).onAdapterChanged(this, oldAdapter, adapter);
            }
        }
    }
    
    

    3.3 populate()调用分析

    1、查看源码可知:一共出现了9次被调用时机。

    1、在ViewPager(@NonNull Context context, @Nullable AttributeSet attrs)方法的局部内部类中被调用。

    2、在setAdapter(@Nullable PagerAdapter adapter)方法中被调用。

    3、在setCurrentItemInternal(int , boolean , boolean , int )方法中被调用。

    4、在setPageTransformer(boolean , @Nullable, int)方法中被调用。

    5、setOffscreenPageLimit(int limit)方法中被调用。

    6、在smoothScrollTo(int x, int y, int velocity)方法中被调用。

    7、onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法中被调用。

    8、在onInterceptTouchEvent(MotionEvent ev)方法中被调用,点击(按下)可以触发。

    9、在onTouchEvent(MotionEvent ev)方法中被调用,点击(按下)可以触发。

    2、预加载Fragment实例源码分析

    从APP启动开始调试,先是调用了setOffscreenPageLimit(int limit)函数中,接着走到了onMeasure(int widthMeasureSpec, int heightMeasureSpec),在这个方法中的调用时候,真正的实现了预加载Fragment实例。即调用了addNewItem(int,int)方法实现预加载。

    第一次调用addNewItem(int position ,int index),将position=0index=0的那个对象添加了进来,

    if (curItem == null && N > 0) {
        curItem = this.addNewItem(this.mCurItem, curIndex);
    }
    
    

    执行完上述条件语句,此时的curItem!=null ,mCurItem==0curIndex==0mItems.size()==1,进而使后 N-1 次调用在一个循环中得以执行:

    if (curItem != null) {
        float extraWidthLeft = 0.f;
        int itemIndex = curIndex - 1;//itemIndex == -1;
    
        ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;//ii==null;
        final int clientWidth = getClientWidth();// 1080
        final float leftWidthNeeded = clientWidth <= 0 ? 0 : 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;//1.0
        for (int pos = mCurItem - 1; pos >= 0; pos--) {
            //此处代码在 mCurItem=0 时不会执行。
        }
    
        float extraWidthRight = curItem.widthFactor;//1.0
        itemIndex = curIndex + 1;//1
        if (extraWidthRight < 2.f) {
            //extraWidthRight=1.0
            ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
            // ii = 1<[1,2,3]?mItems.get([1,2,3])?mItems.get(itemIndex) : null;
            //ii == 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) {
                    //  final int endPos = Math.min(N - 1, mCurItem + pageLimit);
                    //N = 4,mCurItem=0, pageLimit=3;==>endPos=3,不满足条件
    
                 } else if (ii != null && pos == ii.position) {
                    // 不满足条件 ii
                 } else {
                    ii = addNewItem(pos, itemIndex);
                    itemIndex++;//从 1 开始,而mItems中的元素个数也是从 1 开始递增
                    extraWidthRight += ii.widthFactor;
                    ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                    //ii==null
                }
            }
        }
    
        calculatePageOffsets(curItem, curIndex, oldCurInfo);
    }
    
    

    启动时的测量函数同事完成了Fragment的预加载,也就是网上说的缓存,个人觉得叫预缓存或者预加载更贴切些。

    3.4 addNewItem(int,int)函数

    此函数为ViewPager中的函数

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

    从这个方法的代码逻辑可知,
    ItemInfoViewPager中的一个静态内部类,封装了ViewPagerItem的信息。

    static class ItemInfo {
        Object object;
        int position;
        boolean scrolling;
        float widthFactor;
        float offset;
    }
    
    • 属性分析:
      • object :指向新建的item对象
      • position:item的位置
      • scrolling:是否正在滑动
      • widthFactor:宽度因子
      • offset:偏移量

    ii.object = mAdapter.instantiateItem(this, position);每次创建一个item实例,都会被缓存到ItemInfo的对象中。而ii又被添加到了mItems列表中,被缓存起来。

    if (index < 0 || index >= mItems.size()) {
        mItems.add(ii);
    } else {
        mItems.add(index, ii);
    }
    
    private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
    

    综上所述,设置Fragment的缓存数量或者设置适配器的先后顺序,对预缓存是没有影响的。真正完成预加载的逻辑是onMeasure()中实现的。

    4 PagerAdapter实现数据懒加载

    4.1 setUseVisibleHint(boolean)

    此函数是Fragment中的函数,专门被PagerAdapter调用。

    日志截图

    日志截图-设置预加载数为1
    • 该截图是在mViewPager.setOffscreenPageLimit(int limit);的参数为1的情况下进行的。只缓存一个BFragment
    • 此方法在PagerAdapter的子类FragmentStatePagerAdapter中被调用,也就是只有在在viewpagerPagerAdapterfragment结合使用的时候,会触发,触发时机下面分析。
    • 该方法优先于onCreateView()被调用。

    日志截图只是反映了,setUseVisibleHint(boolean)发生了3次调用,且优先于fragmentonCreateView()方法。更详细的信息,需要看源码

    先查看源码,看这个函数在哪里被调用,然后通过调试得到调用时机分析调用逻辑:

    定位定时分析法:源码定位,调试源码定时。

    1、在instantiateItem()中调用了两次,应该就是先去获取预加载的Fragment的实例,此时对用户不可见。

    2、在setPrimaryItem()中调用了一次,对用户可见。

    看一下预加载两个的情况

    mViewPager.setOffscreenPageLimit(2);
    
    
    日志截图-设置预加载数为2

    尾部多出来3条日志,打印的都是AFragment,这是因为mCurItem==0Fragment还是AFragment的缘故。调用了4次,是因为发生了四次onMeasure().为什么调用了四次,留在以后探讨吧。

    mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
    
    

    详细分析,见后面的setPrimaryItem()

    4.2 instantiateItem()

    FragmentStatePagerAdapter.javainstantiateItem()中被调用1次。此时虽然获得了Fragment对象,但是对用户还是不可见状态。

    FragmentStatePagerAdapter.java

    1、代码如下:

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        // If we already have this item instantiated, there is nothing
        // to do.  This can happen when we are restoring the entire pager
        // from its saved state, where the fragment manager has already
        // taken care of restoring the fragments we previously had instantiated.
        if (mFragments.size() > position) {
            Fragment f = mFragments.get(position);
            if (f != null) {
                return f;
            }
        }
    
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
    
        Fragment fragment = getItem(position);//获取Fragment
        if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
        if (mSavedState.size() > position) {
            Fragment.SavedState fss = mSavedState.get(position);
            if (fss != null) {
                fragment.setInitialSavedState(fss);
            }
        }
        while (mFragments.size() <= position) {
            mFragments.add(null);
        }
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
        mFragments.set(position, fragment);
        mCurTransaction.add(container.getId(), fragment);
        return fragment;
    } 
    
    

    2、代码片段分析

    先是判断mFragments里面有没有Fragment,有的话,直接取出并返回。

    // If we already have this item instantiated, there is nothing
    // to do.  This can happen when we are restoring the entire pager
    // from its saved state, where the fragment manager has already
    // taken care of restoring the fragments we previously had instantiated.
    if (mFragments.size() > position) {
        Fragment f = mFragments.get(position);
        if (f != null) {
            return f;
        }
    }
    
    

    看一下mFragment的定义:

    private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
    
    

    查看源码,发现mFragment在初始化Item的函数instantiateItem()中添加了元素。

    while (mFragments.size() <= position) {
        mFragments.add(null);
    }
    fragment.setMenuVisibility(false);
    fragment.setUserVisibleHint(false);
    mFragments.set(position, fragment);
    mCurTransaction.add(container.getId(), fragment
    
    

    由于是启动初始化,所有代码走到这个判断处,不满足条件,直接跳过这段代码,走其下面的代码。

    获取Fragment

    Fragment fragment = getItem(position);//获取Fragment
    
    

    看这一段代码:

    // If we already have this item instantiated, there is nothing
    // to do.  This can happen when we are restoring the entire pager
    // from its saved state, where the fragment manager has already
    // taken care of restoring the fragments we previously had instantiated.
    if (mFragments.size() > position) {
        Fragment f = mFragments.get(position);
        if (f != null) {
            return f;
        }
    }
    
    

    app启动时,position = 0,mFragments.size()==0(这个地方看源码可以知道),所以会直接跳过这段代码,走下面的:

    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }
    
    Fragment fragment = getItem(position);//我们自己实现
    if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
    if (mSavedState.size() > position) {
        Fragment.SavedState fss = mSavedState.get(position);
        if (fss != null) {
            fragment.setInitialSavedState(fss);
        }
    }
    while (mFragments.size() <= position) {
        mFragments.add(null);
    }
    fragment.setMenuVisibility(false);
    fragment.setUserVisibleHint(false);
    mFragments.set(position, fragment);
    mCurTransaction.add(container.getId(), fragment);
    
    return fragment;
    
    

    此时的FragmentTransaction也是null,会走这段代码:

    this.mCurTransaction = this.mFragmentManager.beginTransaction();
    
    

    然后是,通过getItem(0)取出一个fragmentgetItem(int)由我们自己实现:

    用户自己实现的PagerAdapter

    下面这段代码用于恢复状态

    if (mSavedState.size() > position) {
        Fragment.SavedState fss = mSavedState.get(position);
        if (fss != null) {
            fragment.setInitialSavedState(fss);
        }
    }
    
    

    剩下的代码中,我们直接看fragment.setUserVisibleHint(false);

    fragment.setUserVisibleHint(false);
    
    

    就是这段代码,将isVisibleToUser赋值为false,这就是我们懒加载的依据。其实这个时候,我们应该是不加载数据,因为这个时候,视图还没创建,这里写懒加载的逻辑,首先要判断视图是否已创建,即视图不为空

    注意前面的说的APP启动,也就是viewpagerfragment的呈现做的准备工作,即实例化viewpageritem阶段,比如开启事务

    下图是FragmentPagerAdapter.java中代码,分析思路是一样的:

    FragmentPagerAdapter.java

    接下来看另一个调用fragment.setUserVisibleHint(boolean)的函数。

    4.3 setPrimaryItem()

    这个函数,最主要的调用就是被用户滑动切换fragment的时候。

    日志截图的另外一次调用在setPrimaryItem()中。看一下这个函数合适何处被调用。查看源码,发现在populate(int newCurrentItem)函数中被调用,而这个函数被这个populate()调用。
    mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
    一次是给BFragment

    FragmentPagerAdapter.javaFragmentStatePagerAdapter.java一样的实现逻辑:

    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment)object;
        if (fragment != mCurrentPrimaryItem) {
            if (mCurrentPrimaryItem != null) {
                mCurrentPrimaryItem.setMenuVisibility(false);
                mCurrentPrimaryItem.setUserVisibleHint(false);
            }
            if (fragment != null) {
                fragment.setMenuVisibility(true);
                fragment.setUserVisibleHint(true);
            }
            mCurrentPrimaryItem = fragment;
        }
    }
    
    

    参数object说明一下:要切换到的那个fragment,比如从AFragment切换到BFragment,那么object就是BFragment

    这段代码触发的触发有两种:一是系统自动调用:发生在onMeasure()阶段,二是手动:包括点击和切换。

    系统触发

    系统触发显示默认的要展示的fragment,这里指的是AFragment

    调用发生在AFragmentonCreateView()方法之前,对于AFragment:走红框部分,而不走白框。因为这个时候变量fragment就是AFragment,但是this.mCurrentPrimaryItem此时为初始值null,所以会走红框部分。然后,this.mCurrentPrimaryItem = fragment;第一次被赋值,且指向即将可见的AFragment

    image.png
    • 白框部分:系统的调用时候,this.mCurrentPrimaryItem是初始值null,不会走。其他情况出发都会走。并且走白框的时候,就是发生切换的时候,切换是走进这个if语句的前提,当然是除了系统第一次调用这个函数的时候
    • 红框部分:是的this.mCurrentPrimaryItem指向即将可见的fragment

    但是,这个时候,依然没有去创建视图。所以会出现这样的情况:

    image.png

    点击

    点击的时候,只走一行代码,Fragment fragment = (Fragment)object;

    因为点击的时候,object没有发生改变,指向当前fragmentthis.mCurrentPrimaryItem也指向当前fragment

    切换

    切换的时候,会走完这代码块的所有代码:

    image.png
    • 白框部分将即将被切换掉的fragment设置为对用户不可见。
    • 红框部分将即将可见的fragment设置为对用户可见。同时,将this.mCurrentPrimaryItem指向即将可见的fragment

    最终的日志

    image.png

    LazyLoader

    相关文章

      网友评论

        本文标题:ViewPager+Fragment预加载和懒加载分析

        本文链接:https://www.haomeiwen.com/subject/gzgcdctx.html