背景
APP有个列表页,点击列表项可以进入详情页,如果要查看其他列表项需要先返回列表页。为了方便用户快速查看下一项,产品提了一个需求:在详情页左右滑动可以查看上一个或下一个列表项对应的详情页。
在技术方案设计的时候,我准备了两个方案。
方案一:HorizontalPager
因为我们的APP是基于 Fragment + Compose 实现的,所以首先考虑了使用Compose的方案,但使用HorizontalPager对原本Fragment的逻辑改动较大。
方案二:ViewPager + FragmentStateAdapter
使用ViewPager实现,只需要新写一个壳Fragment负责翻页的相关逻辑,不需要改动原本详情页Fragment的代码。并且保持详情页的功能独立,也便于后续的扩展和维护。
最终我采用了方案二。
发现问题
需求上线一段时间后,有一次调试发现,从列表页进去浏览了几个详情页,退回列表页,再进去,上次退出时那些本应该被销毁的详情页Frament居然还在,并且还触发了onResume方法。
我怀疑详情页Fragment可能没有被及时销毁,借助Android Studio的Profiler工具抓取了内存快照,果然发生了内存泄漏。
上图中重点的几处:
- mFragments in GeekDetailPagerAdapter
- this-1 in GeekDetailPagerAdapter-FragmentMaxLifecycleEnforcer-3
- lifecycleRegistry in MainActivity
简单分析了下,详情页Fragment被Adapter引用,Adapter 中的 FragmentMaxLifecycleEnforcer(看名字是个和生命周期相关的组件,具体的作用下面再分析)把什么给注册到 MainActivity 中了,导致Activity一直持有着这些原本应该销毁的Fragment。
在开始排查前,我尝试了几个方案修复,发现在页面销毁时,把viewPager的adapter引用置空,可以解决问题。
为什么把adapter设成null,可以修复内存泄漏?
FragmentMaxLifecycleEnforcer的作用是什么,它跟Activity又有什么联系?
带着这两个问题,我们去从源码中寻找答案。
源码分析
FragmentMaxLifecycleEnforcer 是 FragmentStateAdapter 的内部类,我们就从FragmentStateAdapter 入手。
public abstract class FragmentStateAdapter extends
RecyclerView.Adapter<FragmentViewHolder> implements StatefulAdapter {
@SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor
final Lifecycle mLifecycle;
@SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor
final FragmentManager mFragmentManager;
// Fragment bookkeeping
@SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor
final LongSparseArray<Fragment> mFragments = new LongSparseArray<>();
private FragmentMaxLifecycleEnforcer mFragmentMaxLifecycleEnforcer;
class FragmentMaxLifecycleEnforcer {
// 省略无关代码
}
}
FragmentStateAdapter 继承自 RecyclerView.Adapter,也就是说ViewPager2内部实际上是通过RecyclerView来实现横向翻页的,在ViewPager2的代码中也能发现他持有了一个mRecyclerView对象。
子Fragment 被 mFragments 对象所持有,通过前面的引用栈能猜到,mFragments 在 FragmentMaxLifecycleEnforcer 中被引用了,看了下代码,在这个 Enforcer的 updateFragmentMaxLifecycle方法,确实一直在操作 mFragments。
但如果 FragmentMaxLifecycleEnforcer 能够被及时释放,mFragments也就不会一直被引用了。接下来就是要找 FragmentMaxLifecycleEnforcer 被释放是时机。
@CallSuper
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
checkArgument(mFragmentMaxLifecycleEnforcer == null);
mFragmentMaxLifecycleEnforcer = new FragmentMaxLifecycleEnforcer();
mFragmentMaxLifecycleEnforcer.register(recyclerView);
}
@CallSuper
@Override
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
mFragmentMaxLifecycleEnforcer.unregister(recyclerView);
mFragmentMaxLifecycleEnforcer = null;
}
搜索代码发现,FragmentStateAdapter 中有 onAttachedToRecyclerView 和 onDetachedFromRecyclerView 两个方法。在attached的时候创建了Enforcer并注册了recyclerView。在detached的时候将这个生命周期管理器与recyclerView进行了解绑,并置空引用。
发生了内存泄漏也就表明,onDetachedFromRecyclerView 方法没有被调用,recyclerView没有被正常从Enforcer中注销。接下来就看 onDetachedFromRecyclerView 是在什么时候被调用的。
因为sdk源码无法直接通过点击查找到调用处,也无法全局搜索到,只能通过类内部搜索去查找。
因为这个是Adapter中的方法,首先猜测是在ViewPager2中调用的,看了下并没有。又想起来它是基于 RecyclerView.Adapter 实现的,可以在RecyclerView中找一下,不出意外在RecyclerView中找到了。
private void setAdapterInternal(@Nullable Adapter<?> adapter, boolean compatibleWithPrevious,boolean removeAndRecycleViews) {
if (mAdapter != null) {
mAdapter.unregisterAdapterDataObserver(mObserver);
mAdapter.onDetachedFromRecyclerView(this);
}
if (!compatibleWithPrevious || removeAndRecycleViews) {
removeAndRecycleViews();
}
mAdapterHelper.reset();
final Adapter<?> oldAdapter = mAdapter;
mAdapter = adapter;
if (adapter != null) {
adapter.registerAdapterDataObserver(mObserver);
adapter.onAttachedToRecyclerView(this);
}
if (mLayout != null) {
mLayout.onAdapterChanged(oldAdapter, mAdapter);
}
mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
mState.mStructureChanged = true;
}
在 RecyclerView 的 setAdapterInternal 方法中,每次设置新的适配器时,会detach旧的适配器,调用了 mAdapter.onDetachedFromRecyclerView(this) 进行解绑,这是个private方法。
public void swapAdapter(@Nullable Adapter adapter, boolean removeAndRecycleExistingViews) {
// bail out if layout is frozen
setLayoutFrozen(false);
setAdapterInternal(adapter, true, removeAndRecycleExistingViews);
processDataSetCompletelyChanged(true);
requestLayout();
}
public void setAdapter(@Nullable Adapter adapter) {
// bail out if layout is frozen
setLayoutFrozen(false);
setAdapterInternal(adapter, false, true);
processDataSetCompletelyChanged(false);
requestLayout();
}
继续往上层找,发现了两个public方法,swapAdapter 和 setAdapter,都是用来设置适配器的,也就是sdk提供给开发者将Adapter设置到RecyclerView中的方法。
public void setAdapter(@Nullable @SuppressWarnings("rawtypes") Adapter adapter) {
// 省略无关代码……
mRecyclerView.setAdapter(adapter);
// 省略无关代码……
}
再看 ViewPager2 的 setAdapter 方法,它实际就是调用了 RecyclerView 的 setAdapter 方法。
这下全部都串起来了。调用 ViewPager 的 setAdapter 方法,它会调 RecyclerView 的 setAdapter 方法。而RecyclerView 在设置适配器时,会将旧的 Adapter与当前RecyclerViwe进行解绑。在解绑时,Adapter内部的 FragmentMaxLifecycleEnforcer 也会被Adapter 释放。Enforcer释放就不会一直存在对 mFragments 的引用。
这也就解释了为什么设置 viewPager.adapter = null 可以修复这个内存泄漏。
到这里好像真相大白了,但是又好像哪里不太对劲。
viewPager.adapter = null,这行代码看着就很奇怪,仔细回忆了一下以前使用ViewPager或者RecyclerView,好像没有做过类似的处理。当页面退出,视图销毁的时候,滑动组件它自己会把该释放的对象释放了,不需要开发者自己去对适配器置空。
其实我们只是知道了为什么 viewPager.adapter = null 能够修复,仍然不清楚内存泄漏的原因。
继续分析,上面我们对于 FragmentMaxLifecycleEnforcer 只是匆匆带过,并没有去分析它是怎么工作的,以及为什么它会和MainActivity扯上关系。下面贴上 FragmentMaxLifecycleEnforcer 的核心代码分析下。
class FragmentMaxLifecycleEnforcer {
private ViewPager2.OnPageChangeCallback mPageChangeCallback;
private RecyclerView.AdapterDataObserver mDataObserver;
private LifecycleEventObserver mLifecycleObserver;
private ViewPager2 mViewPager;
private long mPrimaryItemId = NO_ID;
void register(@NonNull RecyclerView recyclerView) {
mViewPager = inferViewPager(recyclerView);
// signal 1 of 3: current item has changed
mPageChangeCallback = new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageScrollStateChanged(int state) {
updateFragmentMaxLifecycle(false);
}
@Override
public void onPageSelected(int position) {
updateFragmentMaxLifecycle(false);
}
};
mViewPager.registerOnPageChangeCallback(mPageChangeCallback);
// signal 2 of 3: underlying data-set has been updated
mDataObserver = new DataSetChangeObserver() {
@Override
public void onChanged() {
updateFragmentMaxLifecycle(true);
}
};
registerAdapterDataObserver(mDataObserver);
// signal 3 of 3: we may have to catch-up after being in a lifecycle state that
// prevented us to perform transactions
mLifecycleObserver = new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
updateFragmentMaxLifecycle(false);
}
};
mLifecycle.addObserver(mLifecycleObserver);
}
void unregister(@NonNull RecyclerView recyclerView) {
ViewPager2 viewPager = inferViewPager(recyclerView);
viewPager.unregisterOnPageChangeCallback(mPageChangeCallback);
unregisterAdapterDataObserver(mDataObserver);
mLifecycle.removeObserver(mLifecycleObserver);
mViewPager = null;
}
void updateFragmentMaxLifecycle(boolean dataSetChanged) {
// 省略无关代码……
Fragment currentItemFragment = mFragments.get(currentItemId);
if (currentItemFragment == null || !currentItemFragment.isAdded()) {
return;
}
mPrimaryItemId = currentItemId;
FragmentTransaction transaction = mFragmentManager.beginTransaction();
Fragment toResume = null;
for (int ix = 0; ix < mFragments.size(); ix++) {
long itemId = mFragments.keyAt(ix);
Fragment fragment = mFragments.valueAt(ix);
if (!fragment.isAdded()) {
continue;
}
if (itemId != mPrimaryItemId) {
transaction.setMaxLifecycle(fragment, STARTED);
} else {
toResume = fragment; // itemId map key, so only one can match the predicate
}
fragment.setMenuVisibility(itemId == mPrimaryItemId);
}
if (toResume != null) { // in case the Fragment wasn't added yet
transaction.setMaxLifecycle(toResume, RESUMED);
}
if (!transaction.isEmpty()) {
transaction.commitNow();
}
}
@NonNull
private ViewPager2 inferViewPager(@NonNull RecyclerView recyclerView) {
ViewParent parent = recyclerView.getParent();
if (parent instanceof ViewPager2) {
return (ViewPager2) parent;
}
throw new IllegalStateException("Expected ViewPager2 instance. Got: " + parent);
}
}
这个类的核心的是 updateFragmentMaxLifecycle 方法。大致浏览完这个方法,再结合方法名和类名就可以猜到十之八九。这个所谓的Enforcer,就是Adapter用来管理所有子Fragment的生命周期的,通过 setMaxLifecycle,通知子Fragment它的生命周期。也就是说ViewPager下的Fragment,它的生命周期函数触发的实际跟常规的Fragment是不太一样的,它的生命周期是由Apdater来管理的。
这也解释了开头提到的一个奇怪的现象:就算旧的fragment实例没被销毁,它也是处于不可见的状态,为什么会触发onResume?
它的onResume回调是由Adapter控制的,至于为什么它不可见了Adapter还会触发它的onResume,跟我们的主题关系不大,这里没有继续深究,感兴趣的朋友可以研究下。
继续回到 FragmentMaxLifecycleEnforcer,除了核心方法 updateFragmentMaxLifecycle,它还有 register 和 unregister 方法。
在register方法中,它给 ViewPager设置了页面切换的回调,在切换页面时调用 updateFragmentMaxLifecycle 方法通知子Fragment更新生命周期。通过 inferViewPager 方法可以知道这里的ViewPager就是我们创建的那个ViewPager。此外还设置了 registerAdapterDataObserver,最重要的来了,它给 mLifecycle 对象设置了一个Observer,在 mLifecycle 的生命周期变化时,也会调用 updateFragmentMaxLifecycle。
在 unregister 方法中,它会移除掉这些监听,包括 mLifecycle。
还记得在内存泄漏引用栈中有一条:lifecycleRegistry in MainActivity。所以这个mLifecycle的嫌疑很大,应该排查一下它。追溯一下发现,这个mLifecycle就是我们在最上面贴的 FragmentStateAdapter 代码中适配器所持有的 mLifecycle。精简后的代码如下:
public abstract class FragmentStateAdapter extends
RecyclerView.Adapter<FragmentViewHolder> implements StatefulAdapter {
@SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor
final Lifecycle mLifecycle;
private FragmentMaxLifecycleEnforcer mFragmentMaxLifecycleEnforcer;
public FragmentStateAdapter(@NonNull FragmentActivity fragmentActivity) {
this(fragmentActivity.getSupportFragmentManager(), fragmentActivity.getLifecycle());
}
public FragmentStateAdapter(@NonNull Fragment fragment) {
this(fragment.getChildFragmentManager(), fragment.getLifecycle());
}
public FragmentStateAdapter(@NonNull FragmentManager fragmentManager,@NonNull Lifecycle lifecycle) {
mFragmentManager = fragmentManager;
mLifecycle = lifecycle;
super.setHasStableIds(true);
}
}
mLifecycle是在 FragmentStateAdapter 的构造函数里赋值的。构造函数有 FragmentActivity类型的参数,还有 Fragment类型的参数,再通过 getLifecycle() 获取它们对应的生命周期对象。根据你调用不同的构造函数,mLifecycle对应的就是Activity的生命周期或者Fragment的生命周期。
对于传统的安卓APP架构,每个页面一般都是单独的Activity,调用哪个构造函数通常不会有什么影响。
但是我忘记了我们的项目是使用单Activity+Navigation的架构,全局只有一个Activity,各页面通过Navigation+Fragment进行路由。这种情况下往FragmentStateAdapter的构造函数传入的是Activity,就会出现即使页面退出了,ViewPager下的Fragment也无法被及时销毁。
class GeekDetailPagerAdapter(
activity: FragmentActivity,
val arguments: Bundle?
) : FragmentStateAdapter(activity) {
// 省略无关代码……
}
这下真的真相大白了,最后梳理一下:
- 创建ViewPager的Adapter时,传入了Activity对象,并获取了它的 Lifecycle;
- mLifecycle注册了LifecycleEventObserver,Observer中引用了 FragmentMaxLifecycleEnforcer;
- FragmentMaxLifecycleEnforcer 又引用了 mFragments;
- 在详情页退出的时候,只是壳Fragment退出了,MainActivity的生命周期并没有结束,也就导致了 mLifecycle 相关的destroy逻辑无法执行;
- Adapter就不会被 detachedFromRecyclerView,进而 FragmentMaxLifecycleEnforcer 的 unRegister 也不会被调用;
- 结果就是 mLifecycle 就会一直间接持有着已经退出的那些详情页Fragment。
最终解决方法:
将PagerAdapter构造参数里的FragmentActivity,改为壳Fragment,这样就不需要在onDestroy手动将Adapter置空了。
总结
在单Activity的APP架构下,对于Activity的使用需要格外小心,一不小心可能就会发生内存泄漏。