FragmentStatePagerAdapter源码分析

1,179 阅读7分钟
  1. 有一次码代码的时候,需要在ViewPager中,往最前面插入一个page,在最开始的方案中,使用了ViewPager搭配FragmentStatePagerAdapter,先简单的实现这个功能。

实现FragmentStatePagerAdapter,重写getItemPosition(),方便插入数据变更后定位到对的位置。 初始数据[1,2,3,4,5],添加后数据[6,1,2,3,4,5],还有一个按钮用来跳转。

image.png

这个功能一共搞出来了三个问题:

  1. 添加[6]之后,页面空白。
  2. 添加之后,位置变动到第二页,定位到第一页页面崩溃。
  3. 添加之后,先右滑再左滑页面崩溃。

image.png

image.png

源码分析

虽然三个问题位置不太一样,但是可以肯定的是添加item才是根本原因,所以分析应该从notifyDataSetChanged()进入,看看具体哪里出现了问题。而很容易定位到最后走到了Viewpager.dataSetChanged(),从这个方法开始分析,扒一下ViewPager都做了些什么。

Viewpager.dataSetChanged()

ViewPager的dataSetChanged()方法,是用来通知ViewPager数据适配器中的数据发生改变,在数据发生变化后,ViewPager会刷新整个视图。

具体的实现流程如下:

  1. 在数据适配器中进行数据的增删改查等操作。
  2. 调用notifyDataSetChanged()方法或者相应的notifyItemXXX()方法通知数据发生了变化。
  3. ViewPager监听到数据发生变化后,会清空view缓存,重新绑定适配器。
  4. ViewPager会调用getItemPosition()方法查询所展示的视图当前位置是否还存在,如果不存在,就会调用destroyItem()方法将其销毁。
  5. 如果操作了item,刷新页面,重新布局。
void dataSetChanged() {
    // This method only gets called if our observer is attached, so mAdapter is non-null.
    for (int i = 0; i < mItems.size(); i++) {
        final ItemInfo ii = mItems.get(i);
        //adapter方法获取新位置
        final int newPos = mAdapter.getItemPosition(ii.object);

        if (newPos == PagerAdapter.POSITION_UNCHANGED) {
            //直接跳过
            continue;
        }

        if (newPos == PagerAdapter.POSITION_NONE) {
            //移除所有item
            mItems.remove(i);
            mAdapter.destroyItem(this, ii.position, ii.object);
            needPopulate = true;
            continue;
        }

        if (ii.position != newPos) {
            //设置更新后的position
            if (ii.position == mCurItem) {
                // Our current item changed position. Follow it.
                newCurrItem = newPos;
            }

            ii.position = newPos;
            needPopulate = true;
        }
    }

    if (needPopulate) {
        // Reset our known page widths; populate will recompute them.
        //重新布局
        setCurrentItemInternal(newCurrItem, false, true);
        requestLayout();
    }
}

看了这个方法来分析一下前面添加的例子。

添加之前数据列表[1,2,3,4,5],Item缓存对应[1,2]的数据,对应positions=[0,1]。

添加之后数据列表[6,1,2,3,4,5],Item缓存对应[1,2]的数据,对应positions=[1,2]。

之后重新布局,ViewPager走到populate()方法。

image.png

image.png

Viewpager.populate()

首先,populate(int newCurrentItem)的作用是将当前位置的视图填充满整个屏幕,同时预加载左右相邻的视图。这个方法有一个参数newCurrentItem,表示当前的位置。

在方法内部,首先会获取ViewPager中保存的当前位置,然后判断newCurrentItem和当前位置的差值,根据差值的正负来确定是向左滑动还是向右滑动。接着根据差值的绝对值决定需要添加的视图数量,并通过数据适配器获取需要添加的视图,加入到ViewPager中。最后还需要移除不需要的视图以释放资源,确保ViewPager的性能和内存占用不会过高。

如下面整理伪代码,方法核心流程:

  1. 获取当前item,更新position。
  2. 遍历当前position前面的item,需要展示的创建,不需要展示的销毁。
  3. 遍历当前position后面的item,需要展示的创建,不需要展示的销毁。
  4. 重新设置当前item。
void populate(int newCurrentItem) {
    ItemInfo oldCurInfo = null;
    if (mCurItem != newCurrentItem) {
        oldCurInfo = infoForPosition(mCurItem);
        mCurItem = newCurrentItem;
    }
    
    final int pageLimit = mOffscreenPageLimit;
    final int startPos = Math.max(0, mCurItem - pageLimit);
    final int N = mAdapter.getCount();
    final int endPos = Math.min(N - 1, mCurItem + pageLimit);
    

    //找到当前的Item,跟新position
    // Locate the currently focused item or add it if needed.
    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;
        }
    }
    
    //遍历整个列表,重新布局,移除不在范围中的item,添加创建在范围中的item
    // 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;
        int itemIndex = curIndex - 1;
        ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
        final int clientWidth = getClientWidth();
        final float leftWidthNeeded = clientWidth <= 0 ? 0 :
                2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
        //遍历左边
        for (int pos = mCurItem - 1; pos >= 0; pos--) {
            if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                //小于startPos,全部销毁
                if (ii == null) {
                    break;
                }
                if (pos == ii.position && !ii.scrolling) {
                    mItems.remove(itemIndex);
                    mAdapter.destroyItem(this, pos, ii.object);
                }
            } else if (ii != null && pos == ii.position) {
                //不小于startPos,并且已经存在,直接跳过
            } else {
                //不小于startPos,并且不存在,创建item
                ii = addNewItem(pos, itemIndex + 1);
                
            }
        }

        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) {
                    //大于endPos,全部销毁
                    if (ii == null) {
                        break;
                    }
                    if (pos == ii.position && !ii.scrolling) {
                        mItems.remove(itemIndex);
                        mAdapter.destroyItem(this, pos, ii.object);
                    }
                } else if (ii != null && pos == ii.position) {
                    //不大于endPos,并且存在,直接跳过
                } else {
                    //不大于endPos,并且不存在,创建item
                    ii = addNewItem(pos, itemIndex);
                }
            }
        }
        //重新设置当前item
        mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
    }
    ...
}

继续分析添加的过程,走到这个方法的时候数据列表[6,1,2,3,4,5],Item缓存对应[1,2]的数据,对应positions=[1,2]。

那么对于前面提到的具体实例,在这个方法经历几个步骤:

  1. newCurrentItem=1,对应数据列表第二个数据,缓存列表第一个数据。找出对应缓存。

  2. 遍历左边的数据列表,对应数据[6],需要预加载,创建对应的item,对应方法mAdapter.instantiateItem(this, position)。

  3. 遍历右边的数据列表,对应数据[2,3,4,5],需要预加载[2],已经有缓存,直接跳过。

  4. 重新设置当前item,对应方法mAdapter.setPrimaryItem(this, mCurItem, curItem.object)。

  5. 结束之后数据列表[6,1,2,3,4,5],Item缓存对应[6,1,2]的数据,对应positions=[0,1,2]。

FragmentStatePagerAdapter.instantiateItem()

instantiateItem(@NonNull ViewGroup container, int position)是用于创建指定位置的Fragment并将其添加到ViewPager中的方法。

具体的实现流程如下:

  1. 通过position获取数据适配器中当前位置的Fragment
  2. 如果当前位置的Fragment为null,则调用getItem()方法创建新的Fragment对象。
  3. 将创建或获取到的Fragment添加进container中,并返回该对象的引用。
  4. 最后,ViewPager会保存当前位置的Fragment对象的引用,以便在页面切换或数据变化时使用。
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
    // 1. 通过position获取数据适配器中当前位置的Fragment
    Fragment fragment = mFragments.get(position);
    if (fragment != null) {
        // 4. 如果找到了Fragment,则直接返回其引用
        return fragment;
    }

    // 2. 如果当前位置的Fragment为null,则调用getItem()方法创建新的Fragment对象。
    fragment = getItem(position);

    // 3. 将创建的或获取到的Fragment添加进container中,并返回该对象的引用
    FragmentTransaction ft = mFragmentManager.beginTransaction();
    ft.add(container.getId(), fragment, "f" + position);  // "f" + position 为Fragment的Tag
    ft.attach(fragment);
    ft.commitNowAllowingStateLoss();

    mFragments.put(position, fragment);  // 保存Fragment引用,以便在页面切换或数据变化时使用

    return fragment;
}

mFragments是Adapter缓存对应fragment对象使用的,首先看一下mFragments的结构是一个ArrayList,缓存与显示对应的framgent对象,position作为索引。

那么在添加之前,数据列表[1,2,3,4,5],mFragments对应缓存数据[1,2],对应position=[0,1]。

在添加之后,数据列表为[6,1,2,3,4,5],前面分析到进入instantiateItem方法的时候是需要创建对应[6]的对象,position=0,但是,但是!这个方法首先使用mFragments去查position=0的缓存,拿到了之前的对应[1]的缓存,并且返回了。那么所有的数据已经完全错乱了,数据列表为[6,1,2,3,4,5],mFragments对应缓存数据[1,2],并且它们的position=[0,1]。而对应的在ViewPager中也是错乱的,ViewPager认为自己缓存的Items对应的数据是[6,1,2],对应positon=[0,1,2];但是实际缓存的Items对应的数据是[1,1,2],对应positon=[0,1,2],也就是在不同的位置缓存的相同的Fragment对象。

setPrimaryItem()和destroyItem()

FragmentStatePagerAdapter.setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) 方法是用于设置当前最主要的可见项,通常在页面的变化时触发。具体的实现流程,如果指定位置的Fragment不为当前的主要可见项,那么将其设置为最主要的可见项。

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

FragmentStatePagerAdapter.destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) 方法用于销毁指定位置的Fragment,以便在页面切换或数据变化时有效地清理资源,防止发生内存泄漏。具体的实现流程,将被销毁的Fragment从mFragments列表中移除,并且remove。

@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    Fragment fragment = (Fragment) object;

    // 将被销毁的Fragment从mFragments列表中移除,并且remove。
    mFragments.set(position, null);
    mCurTransaction.remove(fragment);
}

崩溃原因

前面说到添加操作之后,数据列表为[6,1,2,3,4,5],mFragments对应缓存数据[1,2],并且它们的position=[0,1]。而对应的在ViewPager中也是错乱的,ViewPager认为自己缓存的Items对应的数据是[6,1,2],对应positon=[0,1,2];但是实际缓存的Items对应的数据是[1,1,2],对应positon=[0,1,2],也就是在不同的位置缓存的相同的Fragment对象。

回到最初的三个问题:

  1. 添加之后定位到第一个item,这个时候ViewPager数据是[6,1,2],对应positon=[0,1,2],定位到positon=0之后,他需要去回首position=2的item,走到adapter.destroyItem(),adapter的缓存数据[1,2],并且它们的position=[0,1],在调用mFragments.set(position, null)移除缓存的时候,数据越界。

image.png

  1. 先滑动到下一个,定位到position=2,这时ViewPager的数据是[1,2,3],对应position=[1,2,3],因为[6]离开了区域,触发了销毁,但是因为[6]和[1]在Adapter中只有一个fragment,在ViewPager中对应两个,销毁之后再左滑,重新操作这个Framgnet,就崩溃了。

image.png 3. 添加之后不显示

在Viewpager.populate()方法中,计算完成之后更新child的布局参数,ViewPager的数据是[6,1,2],但是Framgnet只有两个,所以这里也只有两个child,这里使用的infoForChild(View child)方法,是从position=0开始遍历,找到对应的item是[6]。简单来说在ViewPager的Childs存储的数据对应的是[6,2],之后在layout过程中,使用的是Childs数据,也就将对应的view绘制到屏幕外面了,所以也就显示不出来数据了。

image.png

总结

崩溃的地方虽然有所不同,但是核心的问题就是ViewPager.Items,Adapter.mFragments,ViewPager.Childs这几个地方缓存的数据完全是错乱了,而导致这个问题原因,可以概括为FragmentStatePagerAdapter使用了positon作为item的索引,在添加的时候position有了变化,就导致了各种错乱问题。