美文网首页
关于ViewPager你所不知道的一些优化

关于ViewPager你所不知道的一些优化

作者: 头秃到底 | 来源:发表于2024-04-21 18:26 被阅读0次

    通过这篇文章你可以了解到

    • 首页打开的的优化策略之一
    • FragmentPagerAdapter刷新机制
    • getItemPosition的用法

    写在前面

    提到ViewPager想必各位同学一点都不陌生,它是Android中最常用的组件之一,一般配合Fragment一起使用。网上关于它的基本使用和常规优化方式也有很多,在这里我就不一一赘述,而是直接进入这篇文章的主题--ViewPager一些新的优化方式

    我获得这项技能的背景

    最近组里做新的Web容器的,一次承载多个H5页面,以实现左右切换,默认展示主会场页,并要达到提升打开率的目标。要达到这个目标,那势必要从加载优化入手,缩短页面的打开时间。 优化的点包括但不限于,Activity初始化、ViewPager和Fragment的初始化、WebView的初始化等等。我做的第一个优化点便是ViewPager相关。

    解决ViewPager默认加载多个Fragment的问题

    ViewPager会默认给我们缓存多个Fragment,这样设计的目的是为了提升左右滑动的流畅度,代价就是会降低首次打开的启动时间。这让一个以打开率为KPI的我来说是不能容忍的!首先想到的解决方案便是懒加载,当Fragment页面可见时,才从网络加载数据并显示出来。这样做还是不能解决其它Fragment被缓存,以导致占用启动时间的问题,那怎么办?既然ViewPager不给我们只加载一个Fragment的机会,那我们强行创造行不行。我首次只往Adapter塞一个Fragment,等加载完成后再调用notifyDataSetChanged方法更新其它页面行不行!

    解决重复刷新的问题

    FragmentPagerAdapter不会销毁已经初始化完毕的Fragment

    那为什么会有重复刷新的问题?且听我慢慢道来

    我们的主会场在ViewPager中的位置是由后端下发的。首次加载单个Fragment,主会场在ViewPager中的位置只能是0,后续更新时根据后端下发的position动态调整其所在的位置。

    //调整主会场位置伪代码
    marketingInfoList.add(new MarketingInfo("www.juejin.com", "主会场"))
    for (int i = 0; i <= 3; i++) {
        //将放在前两个主会场前面
        if (i < 2) {
            marketingInfoList.add(i, new MarketingInfo("www.baidu.com", "模拟" + i));
        } else {
        //后两个往主会场后面添加
            marketingInfoList.add(new MarketingInfo("www.baidu.com", "模拟" + i));
        }
    }
    mPagerAdapter.notifyDataSetChanged();
    //重新设置选中主会场
    mViewPager.setCurrentItem(2);
    
    

    可在实际开发的过程中却发现,主会场重复加载了两次,ViewPager生成了一个新的Fragment去承载主会场。我们的用户元气满满的点开我们的营销页,正准备下单呢,页面突然又重新白屏了一下。留下一句****,愤然离去。作为一名要给公司带来增长价值的开发这是不能接受的!那怎么办呢?分析源码!

    ViewPager源码解析

    instantiateItem方法作用

    ViewPager会通过这个方法将构造Fragment,FragmentManager和Transaction都在这个方法里出现

    public Object instantiateItem(ViewGroup container, int position) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        
        final long itemId = getItemId(position);
    
        //跟据itemId生成fragment名字,通过名字去查找fragment是否加载过
        String name = makeFragmentName(container.getId(), itemId);
        Fragment fragment = mFragmentManager.findFragmentByTag(name);
        //fragment加载过则直接attach,否则的话新生成一个fragment
        if (fragment != null) {
            if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
            mCurTransaction.attach(fragment);
        } else {
            fragment = getItem(position);
            if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
            mCurTransaction.add(container.getId(), fragment,
            makeFragmentName(container.getId(), itemId));
        }
        if (fragment != mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
            fragment.setUserVisibleHint(false);
        }
    
        return fragment;
     }
    
    

    instantiateItem会通过getItemId获取到itemId,再生成与fragment对应的唯一tag,通过tag查找fragment是否加载过。也就是说只要tag相同,无论你点击的是哪个Tab都会加载到同一个fragment。我们再接着查看生成tag的方法makeFragmentName。

    private static String makeFragmentName(int viewId, long id) {
        return "android:switcher:" + viewId + ":" + id;
    }
    
    

    原来Tag就是由instantiateItem传入的viewId和itemId两个值组成,那么我们再看看itemId的生成方式

    public long getItemId(int position) {
        return position;
    }
    
    

    我惊了!更加的简单!也就是说Fragment的唯一Tag是又position决定的。这下刚刚的问题有答案了吧。

    重复刷新的真相与解决

    ViewPager在初始化Fragment时,会根据Tag寻找Fragment,有则直接加载,无则重新生成。主会场首次加载的position是0,后续调整位置后变成了2,导致两次的Tag不一至,所以就出现了重复加载的问题。知道了问题产生的原因,再来想解决办法就好办了。我们可以重写getItem方法,重新定义itemId的生成方式。

     public long getItemId(int position) {
         //可以直接使用后端给页面ID
         return pageId;
         //后端不给也没事,我们自己生成一个
         return data.get(position).getTitle().hashCode();
     }
    
    

    延伸: #getItemPosition方法

    如果不重写getItemId这方法,将页面位置调整后再跳切回旧的位置,还会面临就位置的页面不刷新的问题。举个栗子:

    掘金的position是0,我将它的position改为2,第0个position这个时候设置为百度,会发现首个页面依然是掘金。

    网上给出的答案是重写getItemPosition方法,虽然可以解决问题,但是没有一个能讲明白这个方法的作用,在这里我来补充一下。

    public int getItemPosition(Object object) {
            return POSITION_UNCHANGED;
    }
    
    

    getItemPosition默认返回POSITION_UNCHANGED,表示页面无变化。还有另外一个默认值POSITION_NONE,表示页面不存在。

    ???

    页面指的是哪个页面?调用时机又是什么?还能再返回其它值吗? 各位看官先别急且看我慢慢写来,写帖一段源码:

    void dataSetChanged() {
            // This method only gets called if our observer is attached, so mAdapter is non-null.
    
            final int adapterCount = mAdapter.getCount();
            mExpectedAdapterCount = adapterCount;
            boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1
                    && mItems.size() < adapterCount;
            int newCurrItem = mCurItem;
    
            boolean isUpdating = false;
                //mItems为旧数据的容器
            for (int i = 0; i < mItems.size(); i++) {
                final ItemInfo ii = mItems.get(i);
              //返回刷新之前Tab项所处的位置
                final int newPos = mAdapter.getItemPosition(ii.object);
                //返回的位置等于POSITION_UNCHANGED(-1)表示当前页未有变更,不做任何操作
                if (newPos == PagerAdapter.POSITION_UNCHANGED) {
                    continue;
                }
                //如果返回的位置等于POSITION_NONE(-2)表示当前页Tab项刷新后不存在,需要销毁并重新加载新的页面
                if (newPos == PagerAdapter.POSITION_NONE) {
                    mItems.remove(i);
                    i--;
    
                    if (!isUpdating) {
                        mAdapter.startUpdate(this);
                        isUpdating = true;
                    }
    
                    mAdapter.destroyItem(this, ii.position, ii.object);
                    needPopulate = true;
    
                    if (mCurItem == ii.position) {
                        // Keep the current item in the valid range
                        newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
                        needPopulate = true;
                    }
                    continue;
                }
                //如果当前页的新的位置和和旧位置不等,则说明调整了顺序
                if (ii.position != newPos) {
                //这段代码是将页面定位到刷新之前的打开页,据数据的position和mCurItem相等的话,则表示这个item是之前打开的,赋予它新位置的值
                    if (ii.position == mCurItem) {
                        // Our current item changed position. Follow it.
                        newCurrItem = newPos;
                    }
    
                    ii.position = newPos;
                    needPopulate = true;
                }
            }
    
            if (isUpdating) {
                mAdapter.finishUpdate(this);
            }
    
            Collections.sort(mItems, COMPARATOR);
    
            if (needPopulate) {
                // Reset our known page widths; populate will recompute them.
                final int childCount = getChildCount();
                for (int i = 0; i < childCount; i++) {
                    final View child = getChildAt(i);
                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                    if (!lp.isDecor) {
                        lp.widthFactor = 0.f;
                    }
                }
    
                setCurrentItemInternal(newCurrItem, false, true);
                requestLayout();
            }
        }
    
    

    notifyDataSetChanged方法之后会调用dataSetChanged方法,getItemPosition又是在dataSetChanged方法被调用的。

    调用notifyDataSetChanged的后,会遍历旧的页面,通过getItemPosition方法返回的位置去决定当前遍历到的页面是否需要更新。POSITION_UNCHANGED:表示页面无变化;POSITION_NONE:表示页面不存在,需要销毁,重新加载新的页面。如果返回值返回的是页面具体的位置,则更新当前页在刷新数据后的位置,将Tab栏选中的对应的Tab项选中。

    再结合源码里的注释看,这下明白了吧!

    相关文章

      网友评论

          本文标题:关于ViewPager你所不知道的一些优化

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