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

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

  • 首页打开的的优化策略之一
  • 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动态调整其所在的位置。

java 复制代码
//调整主会场位置伪代码
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都在这个方法里出现

java 复制代码
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。

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

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

java 复制代码
public long getItemId(int position) {
    return position;
}

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

重复刷新的真相与解决

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

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

延伸: #getItemPosition方法

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

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

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

java 复制代码
public int getItemPosition(Object object) {
        return POSITION_UNCHANGED;
}

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

???

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

java 复制代码
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项选中。

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

相关推荐
MiyamuraMiyako1 小时前
从 0 到发布:Gradle 插件双平台(MavenCentral + Plugin Portal)发布记录与避坑
android
NRatel1 小时前
Unity 游戏提升 Android TargetVersion 相关记录
android·游戏·unity·提升版本
叽哥4 小时前
Kotlin学习第 1 课:Kotlin 入门准备:搭建学习环境与认知基础
android·java·kotlin
风往哪边走4 小时前
创建自定义语音录制View
android·前端
用户2018792831674 小时前
事件分发之“官僚主义”?或“绕圈”的艺术
android
用户2018792831674 小时前
Android事件分发为何喜欢“兜圈子”?不做个“敞亮人”!
android
Kapaseker6 小时前
你一定会喜欢的 Compose 形变动画
android
QuZhengRong6 小时前
【数据库】Navicat 导入 Excel 数据乱码问题的解决方法
android·数据库·excel
zhangphil7 小时前
Android Coil3视频封面抽取封面帧存Disk缓存,Kotlin(2)
android·kotlin
程序员码歌14 小时前
【零代码AI编程实战】AI灯塔导航-总结篇
android·前端·后端