首先来看看问题的复现的代码
class InnerPagerAdapter(
fm: FragmentManager,
private val fragments: ArrayList<out Fragment>,
private val titles: List<CharSequence>
) : FragmentStatePagerAdapter(fm) {
override fun getCount(): Int {
return fragments.size
}
override fun getPageTitle(position: Int): CharSequence? {
return titles[position]
}
override fun getItem(position: Int): Fragment {
return fragments[position]
}
override fun getItemPosition(`object`: Any): Int {
return PagerAdapter.POSITION_NONE
}
override fun saveState(): Parcelable? {
return null
}
}
InnerPagerAdapter在配合ViewPager的时候,如果fragment size>2的时候会发生内存泄漏
原因是FragmentStatePagerAdapter在调用destroyItem方法后并没有移除fragments里面的fragment,也就是导致fragment即便是被destroy了,但是实例还在内存中 ,下面是Profiler内存泄漏分析截图
为什么是大于2的时候才会有这个问题?
这里直接给出答案,主要原因在于ViewPager.mOffscreenPageLimit 这个字段
来看看源码
private static final int *DEFAULT_OFFSCREEN_PAGES* = 1;
private int mOffscreenPageLimit = *DEFAULT_OFFSCREEN_PAGES*;
可以看到mOffscreenPageLimit的默认值是1,来看看具体使用到它的方法populate()源码
void populate(int newCurrentItem) {
ItemInfo oldCurInfo = null;
if (mCurItem != newCurrentItem) {
oldCurInfo = infoForPosition(mCurItem);
mCurItem = newCurrentItem;
}
if (mAdapter == null) {
sortChildDrawingOrder();
return;
}
// Bail now if we are waiting to populate. This is to hold off
// on creating views from the time the user releases their finger to
// fling to a new position until we have finished the scroll to
// that position, avoiding glitches from happening at that point.
if (mPopulatePending) {
if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
sortChildDrawingOrder();
return;
}
// Also, don't populate until we are attached to a window. This is to
// avoid trying to populate before we have restored our view hierarchy
// state and conflicting with what is restored.
if (getWindowToken() == null) {
return;
}
mAdapter.startUpdate(this);
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);
if (N != mExpectedAdapterCount) {
String resName;
try {
resName = getResources().getResourceName(getId());
} catch (Resources.NotFoundException e) {
resName = Integer.toHexString(getId());
}
throw new IllegalStateException("The application's PagerAdapter changed the adapter's"
+ " contents without calling PagerAdapter#notifyDataSetChanged!"
+ " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N
+ " Pager id: " + resName
+ " Pager class: " + getClass()
+ " Problematic adapter: " + mAdapter.getClass());
}
// 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;
}
}
if (curItem == null && N > 0) {
curItem = addNewItem(mCurItem, curIndex);
}
// 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) {
if (ii == null) {
break;
}
if (pos == ii.position && !ii.scrolling) {
mItems.remove(itemIndex);
mAdapter.destroyItem(this, pos, ii.object);
if (DEBUG) {
Log.i(TAG, "populate() - destroyItem() with pos: " + pos
+ " view: " + ((View) ii.object));
}
itemIndex--;
curIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {
extraWidthLeft += ii.widthFactor;
itemIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
} else {
ii = addNewItem(pos, itemIndex + 1);
extraWidthLeft += ii.widthFactor;
curIndex++;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
}
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) {
if (ii == null) {
break;
}
if (pos == ii.position && !ii.scrolling) {
mItems.remove(itemIndex);
mAdapter.destroyItem(this, pos, ii.object);
if (DEBUG) {
Log.i(TAG, "populate() - destroyItem() with pos: " + pos
+ " view: " + ((View) ii.object));
}
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {
extraWidthRight += ii.widthFactor;
itemIndex++;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
} else {
ii = addNewItem(pos, itemIndex);
itemIndex++;
extraWidthRight += ii.widthFactor;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
}
}
calculatePageOffsets(curItem, curIndex, oldCurInfo);
mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
}
if (DEBUG) {
Log.i(TAG, "Current page list:");
for (int i = 0; i < mItems.size(); i++) {
Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
}
}
mAdapter.finishUpdate(this);
// Check width measurement of current pages and drawing sort order.
// Update LayoutParams as needed.
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
lp.childIndex = i;
if (!lp.isDecor && lp.widthFactor == 0.f) {
// 0 means requery the adapter for this, it doesn't have a valid width.
final ItemInfo ii = infoForChild(child);
if (ii != null) {
lp.widthFactor = ii.widthFactor;
lp.position = ii.position;
}
}
}
sortChildDrawingOrder();
if (hasFocus()) {
View currentFocused = findFocus();
ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
if (ii == null || ii.position != mCurItem) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
ii = infoForChild(child);
if (ii != null && ii.position == mCurItem) {
if (child.requestFocus(View.FOCUS_FORWARD)) {
break;
}
}
}
}
}
}
populate() 每次会在切换页面的时候调用,由于代码太长,我这里简化了下,主要来看看跟我们这次问题相关的代码
final int pageLimit = mOffscreenPageLimit;
final int startPos = Math.*max*(0, mCurItem - pageLimit);
final int endPos = Math.*min*(N - 1, mCurItem + pageLimit);
final int N = mAdapter.getCount();
for (int pos = mCurItem + 1; pos < N; pos++) {
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
mItems.remove(itemIndex);
mAdapter.destroyItem(this, pos, ii.object);
}
}
for (int pos = mCurItem + 1; pos < N; pos++) {
if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
mItems.remove(itemIndex);
mAdapter.destroyItem(this, pos, ii.object);
}
}
根据pageLimit计算viewpager的页面缓存的开始位置和结束位置,pos<startPos||pos>endPos的页面都会被destroy,当我们在不更改mOffscreenPageLimit的默认配置的时候,在pos<startPos||pos>endPos会被destroy,所以当fragment 的数量<=2 的时候,这2个条件都不会执行
为什么只有ViewPager的adapter使用的是FragmentStatePagerAdapter的时候,才会出现问题?
原因在于FragmentStatePagerAdapter的destroyItem方法,来看看源码
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
Fragment fragment = (Fragment) object;
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
+ " v=" + ((Fragment)object).getView());
while (mSavedState.size() <= position) {
mSavedState.add(null);
}
mSavedState.set(position, fragment.isAdded()
? mFragmentManager.saveFragmentInstanceState(fragment) : null);
mFragments.set(position, null);
mCurTransaction.remove(fragment);
if (fragment.equals(mCurrentPrimaryItem)) {
mCurrentPrimaryItem = null;
}
}
这个方法会调用FragmentManager remove掉fragment,所以Fragment会走onDestroy,那这个fragment就应该没有用了,但是由于被fragments持有引用,内存一直没法回收,就出现了内存泄漏问题
解决方案:
1.直接设置 ViewPager.offscreenPageLimit=fragments.size-1 ,可以让Fragment不被销毁,就不会有内存泄漏问题
2.重写destroyItem方法,调用list 移除Fragment
3.List不能存放fragment引用,可以传Fragment类名,然后根据反射创建fragment,如果要传参数,可以模仿ViewModel的工厂创建方法实现
|