多 Fragment /嵌套并使用 setOffscreenPageLimit 导致内存泄漏的处理
项目背景
原始代码是结构类似于一个 ViewPage2 加上 tab 切换按钮,然后在这个 ViewPage2 里又嵌套了一层 ViewPage2 加上 tab 切换按钮。即 Fragment 嵌套显示 Fragment。同时 ViewPage2 设置了 setOffscreenPageLimit(int limit) 这个方法。这个结构项目较小或页面较少的情况下是可以的,页面初始化速度快,请求比较少,初始化不会太久,但项目大了之后就暴露出问题,页面太多了,初始化要很久。
原始代码
原始代码中,使用的 ViewPagerAdapter 如下,可以看到这里 mFragments 是持有 Fragment 实例的,可能会导致内存泄漏,所以我在 ViewPage2 的使用时调用了 setOffscreenPageLimit(int limit),将我所有 Fragment 都保存下来,相当于弃用了 ViewPage2 的缓存机制。
好处是初始化后所有 Fragment 都可以及时使用,不会出现有时候 Fragment 加载比较慢,出现白屏的问题。
坏处是会预加载所有 Fragment,一开始会占用较多资源,初始化时会一次性将 ViewPage2 中所有 Fragment 初始化至 onStart()。
public class ViewPagerAdapter extends FragmentStateAdapter {
private final List<Fragment> mFragments;
public ViewPagerAdapter(@NonNull FragmentActivity fragmentActivity, List<Fragment> fragmentList) {
super(fragmentActivity);
mFragments = fragmentList;
}
public ViewPagerAdapter(@NonNull Fragment fragment, List<Fragment> fragments) {
super(fragment);
mFragments = fragments;
}
@NonNull
@Override
public Fragment createFragment(int position) {
return mFragments.get(position);
}
@Override
public int getItemCount() {
return null != mFragments ? mFragments.size() : 0;
}
}
private final List<Fragment> mBaseFragment = new ArrayList<>();
···
mAFragment = new ControlFragment(clickListener);
mBFragment = new RecordFragment(clickListener);
mBaseFragment.add(mAFragment);
mBaseFragment.add(mBFragment);
mBaseFragment.add(new C(clickListener));
mBaseFragment.add(new D(clickListener));
mb.vpMain.setAdapter(new ViewPagerAdapter(this, mBaseFragment));
mb.vpMain.setOffscreenPageLimit(mBaseFragment.size());
···
并且有时候我需要直接使用 Fragment 实例进行操作 UI,就导致我实际是需要一直持有这个 Fragment 实例,并保证它不变。这个其实也是不对的。
处理初始化缓慢的根源
我的情况下卡顿是因为使用了 setOffscreenPageLimit(mBaseFragment.size()) ,但是直接去掉这句话,切换页面的时候可能会导致 Fragment 被销毁时,ViewPagerAdapter 还持有要销毁 Fragment 的实例,导致内存泄漏或其他问题。所以 ViewPagerAdapter 也要进行修改,保证不持有具体 Fragment 的实例。
public class ViewPagerAdapter extends FragmentStateAdapter {
private final List<Class<? extends Fragment>> mFragmentClasses;
public ViewPagerAdapter(@NonNull FragmentManager fm, @NonNull Lifecycle lifecycle, List<Class<? extends Fragment>> fragmentClasses) {
super(fm, lifecycle);
this.mFragmentClasses = fragmentClasses;
}
@Override
public int getItemCount() {
return mFragmentClasses.size();
}
@NonNull
@Override
public Fragment createFragment(int position) {
try {
return mFragmentClasses.get(position).newInstance();
} catch (Exception e) {
e.printStackTrace();
return new Fragment();
}
}
}
private final List<Class<? extends Fragment>> mBaseFragment = new ArrayList<>();
···
mBaseFragment.add(A.class);
mBaseFragment.add(B.class);
mBaseFragment.add(C.class);
mBaseFragment.add(D.class);
mb.vpMain.setAdapter(new ViewPagerAdapter(getSupportFragmentManager(), getLifecycle(), mBaseFragment));
// mb.vpMain.setOffscreenPageLimit(mBaseFragment.size());
···
这边修改为只保存 Fragment 的类名,有需要的时候自动创建实例,ViewPagerAdapter 本身不持有 Fragment 的实例,调用这个 ViewPage2的 Activity 或 Fragment 也不持有 Fragment 的实例,即现在 mBaseFragment 这里存放的也只是类名。
之前为了便于操作直接保存实例来调用,后面看了下其实也只是数据变动的时候需要使用 Fragment 的实例进行对应 UI 变动而已,比如外部 Fragment 在构造内部 Fragment 的时传入一个点击事件,使得内部 Fragment 的 view 可以通过点击事件来调用外部 Fragment 的 ViewPage2 切换,实现类似于面包屑的功能。这边点击事件可能传入匿名内部类,会使得默认持有外部 Fragment 的引用,内部销毁时候还要释放点击事件,比较麻烦。这块代码也改成了使用 ViewModel 来自动更新 UI。
处理之后又发现新的问题
经过处理,理论上本身 Activity 或 Fragment 或 ViewPagerAdapter 存放的只是 Fragment 的类名,不持有 Fragment 实例,不会导致内存泄漏,但实际用 LeakCanary 发现 Fragment 销毁后还是出现了 Fragment 销毁不掉导致内存泄漏的问题,但此时提示的是 ViewPager2 有泄漏。
final List<Class<? extends Fragment>> fragments = new ArrayList<>();
fragments.add(A.class);
fragments.add(B.class);
mViewBinding.viewPage.setAdapter(new ViewPagerAdapter2(mActivity.getSupportFragmentManager(), getLifecycle(), fragments));
经过排查,最后原因其实很简单,因为我存在 Fragment 嵌套的使用,在 Fragment 使用 ViewPagerAdapter 的时候,构造方法第一个参数想当然地使用了 Activity 的 getSupportFragmentManager(),而这会导致 ViewPager2 内部 Fragment 不能正确回收,因为现在 ViewPager2 内部的 Fragment 添加到 宿主 Activity 的 FragmentManager,而不是当前 Fragment 的 ChildFragmentManager。这样 ViewPager2 即使销毁了,Fragment 仍然被 Activity 管理,不会自动回收,造成内存泄漏。
完整解决方案
- ViewPage2 所用的 FragmentStateAdapter 里面最好不要自己再去维护具体的 Fragment 实例,最好是依赖默认构造函数在需要的时候自动创建。
- 如果要其他地方使用到 Fragment 实例来修改 UI,就改成使用 ViewModel ,在具体响应的 onStart() 中增加 observe 来观察数据变动,数据变动后再对 Fragment 的 UI 进行变动。这样生命周期不需要额外维护,UI 变动也及时,也不存在实例还需要销毁的问题。
- 同时尽量精简 onAttach/onCreate/onCreateView/onActivityCreated/onStart 里做的耗时操作,加快页面显示。
public class ViewPagerAdapter extends FragmentStateAdapter {
private final List<Class<? extends Fragment>> mFragmentClasses;
/**
* ViewPager 适配器,用于根据 Fragment 类创建实例。
*
* @param fm FragmentManager 用于管理 Fragment。
* <p><b>注意:</b>如果此适配器用于 Fragment 内部嵌套 ViewPager2,
* 请使用 getChildFragmentManager(),否则可能导致 Fragment 不能正确回收。</p>
* @param lifecycle 生命周期对象,用于绑定 Fragment 生命周期。
* @param fragmentClasses 需要显示的 Fragment 类列表,每个 Fragment 需要有无参构造方法。
*/
public ViewPagerAdapter2(@NonNull FragmentManager fm, @NonNull Lifecycle lifecycle, List<Class<? extends Fragment>> fragmentClasses) {
super(fm, lifecycle);
this.mFragmentClasses = fragmentClasses;
}
@Override
public int getItemCount() {
return mFragmentClasses.size();
}
@NonNull
@Override
public Fragment createFragment(int position) {
try {
return mFragmentClasses.get(position).newInstance();
} catch (Exception e) {
e.printStackTrace();
return new Fragment();
}
}
}
/*Ativity*/
private final List<Class<? extends Fragment>> mBaseFragment = new ArrayList<>();
···
mBaseFragment.add(A.class);
mBaseFragment.add(B.class);
mb.vpMain.setAdapter(new ViewPagerAdapter(getSupportFragmentManager(), getLifecycle(), mBaseFragment));
/*Fragment*/
private final List<Class<? extends Fragment>> mBaseFragment = new ArrayList<>();
···
mBaseFragment.add(A.class);
mBaseFragment.add(B.class);
mb.vpMain.setAdapter(new ViewPagerAdapter(getChildFragmentManager(), getLifecycle(), mBaseFragment));
最后
ViewPage2 最好避免使用 setOffscreenPageLimit(int limit),这个相当于是弃用或者自定义缓存多少 Fragment 页面。也不要将太重的操作放在 Fragment,要将其视为随时会被销毁的,能随时新建并还原至离开时状态的。
多 Fragment 使用 setOffscreenPageLimit(int limit),会初始化所有 Fragment,没什么必要。
如果嵌套使用,要注意宿主 Activity 的 FragmentManager,和 Fragment 的 ChildFragmentManager 之间的区别。
3445

被折叠的 条评论
为什么被折叠?



