FragmentStatePagerAdapter使用不当引起的内存泄漏问题

首先来看看问题的复现的代码

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的工厂创建方法实现

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值