- 有一次码代码的时候,需要在ViewPager中,往最前面插入一个page,在最开始的方案中,使用了ViewPager搭配FragmentStatePagerAdapter,先简单的实现这个功能。
实现FragmentStatePagerAdapter,重写getItemPosition(),方便插入数据变更后定位到对的位置。
初始数据[1,2,3,4,5],添加后数据[6,1,2,3,4,5],还有一个按钮用来跳转。
这个功能一共搞出来了三个问题:
- 添加[6]之后,页面空白。
- 添加之后,位置变动到第二页,定位到第一页页面崩溃。
- 添加之后,先右滑再左滑页面崩溃。
源码分析
虽然三个问题位置不太一样,但是可以肯定的是添加item才是根本原因,所以分析应该从notifyDataSetChanged()进入,看看具体哪里出现了问题。而很容易定位到最后走到了Viewpager.dataSetChanged(),从这个方法开始分析,扒一下ViewPager都做了些什么。
Viewpager.dataSetChanged()
ViewPager的dataSetChanged()方法,是用来通知ViewPager数据适配器中的数据发生改变,在数据发生变化后,ViewPager会刷新整个视图。
具体的实现流程如下:
- 在数据适配器中进行数据的增删改查等操作。
- 调用notifyDataSetChanged()方法或者相应的notifyItemXXX()方法通知数据发生了变化。
- ViewPager监听到数据发生变化后,会清空view缓存,重新绑定适配器。
- ViewPager会调用getItemPosition()方法查询所展示的视图当前位置是否还存在,如果不存在,就会调用destroyItem()方法将其销毁。
- 如果操作了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()方法。
Viewpager.populate()
首先,populate(int newCurrentItem)的作用是将当前位置的视图填充满整个屏幕,同时预加载左右相邻的视图。这个方法有一个参数newCurrentItem,表示当前的位置。
在方法内部,首先会获取ViewPager中保存的当前位置,然后判断newCurrentItem和当前位置的差值,根据差值的正负来确定是向左滑动还是向右滑动。接着根据差值的绝对值决定需要添加的视图数量,并通过数据适配器获取需要添加的视图,加入到ViewPager中。最后还需要移除不需要的视图以释放资源,确保ViewPager的性能和内存占用不会过高。
如下面整理伪代码,方法核心流程:
- 获取当前item,更新position。
- 遍历当前position前面的item,需要展示的创建,不需要展示的销毁。
- 遍历当前position后面的item,需要展示的创建,不需要展示的销毁。
- 重新设置当前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]。
那么对于前面提到的具体实例,在这个方法经历几个步骤:
-
newCurrentItem=1,对应数据列表第二个数据,缓存列表第一个数据。找出对应缓存。
-
遍历左边的数据列表,对应数据[6],需要预加载,创建对应的item,对应方法mAdapter.instantiateItem(this, position)。
-
遍历右边的数据列表,对应数据[2,3,4,5],需要预加载[2],已经有缓存,直接跳过。
-
重新设置当前item,对应方法mAdapter.setPrimaryItem(this, mCurItem, curItem.object)。
-
结束之后数据列表[6,1,2,3,4,5],Item缓存对应[6,1,2]的数据,对应positions=[0,1,2]。
FragmentStatePagerAdapter.instantiateItem()
instantiateItem(@NonNull ViewGroup container, int position)是用于创建指定位置的Fragment并将其添加到ViewPager中的方法。
具体的实现流程如下:
- 通过position获取数据适配器中当前位置的Fragment
- 如果当前位置的Fragment为null,则调用getItem()方法创建新的Fragment对象。
- 将创建或获取到的Fragment添加进container中,并返回该对象的引用。
- 最后,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对象。
回到最初的三个问题:
- 添加之后定位到第一个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)移除缓存的时候,数据越界。
- 先滑动到下一个,定位到position=2,这时ViewPager的数据是[1,2,3],对应position=[1,2,3],因为[6]离开了区域,触发了销毁,但是因为[6]和[1]在Adapter中只有一个fragment,在ViewPager中对应两个,销毁之后再左滑,重新操作这个Framgnet,就崩溃了。
3. 添加之后不显示
在Viewpager.populate()方法中,计算完成之后更新child的布局参数,ViewPager的数据是[6,1,2],但是Framgnet只有两个,所以这里也只有两个child,这里使用的infoForChild(View child)方法,是从position=0开始遍历,找到对应的item是[6]。简单来说在ViewPager的Childs存储的数据对应的是[6,2],之后在layout过程中,使用的是Childs数据,也就将对应的view绘制到屏幕外面了,所以也就显示不出来数据了。
总结
崩溃的地方虽然有所不同,但是核心的问题就是ViewPager.Items,Adapter.mFragments,ViewPager.Childs这几个地方缓存的数据完全是错乱了,而导致这个问题原因,可以概括为FragmentStatePagerAdapter使用了positon作为item的索引,在添加的时候position有了变化,就导致了各种错乱问题。