最近做需求的时候,遇到ViewPager+FragmentPagerAdapter场景下面Fragment复用的问题。甚至我重新初始化Adapter,重新设置Adapter还是没有作用。
背景介绍
我们主页的结构如上图,一个TabLayout+ViewPager, Viewpager里面使用Fragment。这应该是比较常见的结构,页面支持下拉刷新,TabLayout的内容会变化,Fragment的内容也会随着TabLayout变化而变化。 当所有数据变化的时候,我重新new 一个Adapter也Fragment会被复用,并不是重新初始化新的Fragment. 导致上一个数据某些状态标志被复用,导致出现问题。
定位原因
打开FragmentPagerAdapter
源码
public abstract class FragmentPagerAdapter extends PagerAdapter {
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
//...
final long itemId = getItemId(position);
//获取TAG标识
String name = makeFragmentName(container.getId(), itemId);
//查找缓存
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
//....
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
//....
return fragment;
}
//默认标识为位置
public long getItemId(int position) {
return position;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
Fragment fragment = (Fragment) object;
//销毁的时候。执行detach
mCurTransaction.detach(fragment);
}
}
//组成缓存key
private static String makeFragmentName(int viewId, long id) {
return "android:switcher:" + viewId + ":" + id;
}
我们可以看到几个重要信息
- 缓存在FragmentManger中,每次取Item的时候,从FragmentManager 的缓存中找,没有找到才调用 getItem(position) 获取对应Fragment
- 缓存的key 为 父容器id + getItemId 默认实现返回position
- destroyItem的时候,使用的是detach
接下来我们看下FragmentManager的代码
FragmentStateManager addFragment(@NonNull Fragment fragment) {
//...
FragmentStateManager fragmentStateManager = createOrGetFragmentStateManager(fragment);
fragment.mFragmentManager = this;
// 添加到FragmentStore#mActive数组中
mFragmentStore.makeActive(fragmentStateManager);
if (!fragment.mDetached) {
// 添加到FragmentStore#mAdded数组中
mFragmentStore.addFragment(fragment);
//...
}
return fragmentStateManager;
}
void removeFragment(@NonNull Fragment fragment) {
final boolean inactive = !fragment.isInBackStack();
if (!fragment.mDetached || inactive) {
// 清除FragmentStore#mAdded数组
mFragmentStore.removeFragment(fragment);
// mRemoving 标志位为true
fragment.mRemoving = true;
//...
}
}
void detachFragment(@NonNull Fragment fragment) {
if (!fragment.mDetached) {
fragment.mDetached = true;
if (fragment.mAdded) {
//清除FragmentStore#mAdded数组
mFragmentStore.removeFragment(fragment);
//...
}
}
}
可以重点关注下面代码
mFragmentStore#makeActive ---> 放到 FragmentStore#mActive数组中
mFragmentStore#addFragment ---> 放到 FragmentStore#mAdded数组中
mFragmentStore#removeFragment ---> 清除FragmentStore#mAdded数组
什么情况清空 mActive集合?
查看代码只有Fragment#removing 为 true的时候,才会清空
void moveToExpectedState() {
Fragment f = fragmentStateManager.getFragment();
boolean beingRemoved = f.mRemoving && !f.isInBackStack();
if (beingRemoved) {
makeInactive(fragmentStateManager);
}
}
总结原因
本质是因为FragmentPagerAdapter#destroyItem的时候,只调用了detach ,导致Fragment一直缓存在FragmentStore#mActive数组中,一直不能释放。
解决问题
我们需要解决FragmentPagerAdapter 两个问题
- FragmentPagerAdapter与position 绑定,而不是与数据绑定
- FragmentPagerAdapter的Fragment无法删除
解决方案:
- 切到到ViewPager2+FragmentStateAdapter中(它调用的remove方法)
- 修改FragmentPagerAdapter
因为切换ViewPager2+FragmentStateAdapter 影响较大,并且影响我们项目中的一些滑动冲突,所以权衡一下决定copy FragmentPagerAdapter 进行修改。加一个LRUCache控制缓存数量,执行销毁逻辑,重写makeFragmentName与数据绑定。
代码如下
import android.os.Parcelable;
import android.util.Log;
import android.util.LruCache;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.Lifecycle;
import androidx.viewpager.widget.PagerAdapter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* 拷贝自 FragmentPagerAdapter代码
* 修改地方:
* 1. makeFragmentName 修改成protected, 子类可以继承修改
* 2. 增加LruCache缓存,防止fragment的被缓存以及及时清理掉Fragment
*/
public abstract class BaseFragmentPagerAdapter extends PagerAdapter {
private static final String TAG = "FragmentPagerAdapter";
private static final boolean DEBUG = false;
private static final int DEFAULT_CACHE_SIZE = 10;
private final LruCache<String, Fragment> mFragmentCache = new FragmentCache(DEFAULT_CACHE_SIZE);
@Retention(RetentionPolicy.SOURCE)
@IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT})
private @interface Behavior {
}
/**
* Indicates that {@link Fragment#setUserVisibleHint(boolean)} will be called when the current
* fragment changes.
*
* @deprecated This behavior relies on the deprecated
* {@link Fragment#setUserVisibleHint(boolean)} API. Use
* {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} to switch to its replacement,
* {@link FragmentTransaction#setMaxLifecycle}.
*/
@Deprecated
public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0;
/**
* Indicates that only the current fragment will be in the {@link Lifecycle.State#RESUMED}
* state. All other Fragments are capped at {@link Lifecycle.State#STARTED}.
*/
public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1;
private final FragmentManager mFragmentManager;
private final int mBehavior;
private FragmentTransaction mCurTransaction = null;
private Fragment mCurrentPrimaryItem = null;
private boolean mExecutingFinishUpdate;
/**
* Constructor for {@link FragmentPagerAdapter} that sets the fragment manager for the adapter.
* This is the equivalent of calling {@link #BaseFragmentPagerAdapter(FragmentManager, int)} and
* passing in {@link #BEHAVIOR_SET_USER_VISIBLE_HINT}.
*
* <p>Fragments will have {@link Fragment#setUserVisibleHint(boolean)} called whenever the
* current Fragment changes.</p>
*
* @param fm fragment manager that will interact with this adapter
* @deprecated use {@link #BaseFragmentPagerAdapter(FragmentManager, int)} with
* {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT}
*/
@Deprecated
public BaseFragmentPagerAdapter(@NonNull FragmentManager fm) {
this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
}
/**
* Constructor for {@link FragmentPagerAdapter}.
* <p>
* If {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} is passed in, then only the current
* Fragment is in the {@link Lifecycle.State#RESUMED} state. All other fragments are capped at
* {@link Lifecycle.State#STARTED}. If {@link #BEHAVIOR_SET_USER_VISIBLE_HINT} is passed, all
* fragments are in the {@link Lifecycle.State#RESUMED} state and there will be callbacks to
* {@link Fragment#setUserVisibleHint(boolean)}.
*
* @param fm fragment manager that will interact with this adapter
* @param behavior determines if only current fragments are in a resumed state
*/
public BaseFragmentPagerAdapter(@NonNull FragmentManager fm,
@Behavior int behavior) {
mFragmentManager = fm;
mBehavior = behavior;
}
/**
* Return the Fragment associated with a specified position.
*/
@NonNull
public abstract Fragment getItem(int position);
@Override
public void startUpdate(@NonNull ViewGroup container) {
if (container.getId() == View.NO_ID) {
throw new IllegalStateException("ViewPager with adapter " + this
+ " requires a view id");
}
}
@SuppressWarnings({"ReferenceEquality", "deprecation"})
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), position);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + position + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
mFragmentCache.put(name, fragment); //添加到缓存中
if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), position));
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
} else {
fragment.setUserVisibleHint(false);
}
}
return fragment;
}
// TODO(b/141958824): Suppressed during upgrade to AGP 3.6.
@SuppressWarnings("ReferenceEquality")
@Override
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, "Detaching item #" + getItemId(position) + ": f=" + object
+ " v=" + fragment.getView());
mCurTransaction.detach(fragment);
if (fragment.equals(mCurrentPrimaryItem)) {
mCurrentPrimaryItem = null;
}
}
@SuppressWarnings({"ReferenceEquality", "deprecation"})
@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);
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
} else {
mCurrentPrimaryItem.setUserVisibleHint(false);
}
}
fragment.setMenuVisibility(true);
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
} else {
fragment.setUserVisibleHint(true);
}
mCurrentPrimaryItem = fragment;
}
}
@Override
public void finishUpdate(@NonNull ViewGroup container) {
if (mCurTransaction != null) {
// We drop any transactions that attempt to be committed
// from a re-entrant call to finishUpdate(). We need to
// do this as a workaround for Robolectric running measure/layout
// calls inline rather than allowing them to be posted
// as they would on a real device.
if (!mExecutingFinishUpdate) {
try {
mExecutingFinishUpdate = true;
mCurTransaction.commitNowAllowingStateLoss();
} finally {
mExecutingFinishUpdate = false;
}
}
mCurTransaction = null;
}
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return ((Fragment) object).getView() == view;
}
@Override
@Nullable
public Parcelable saveState() {
return null;
}
@Override
public void restoreState(@Nullable Parcelable state, @Nullable ClassLoader loader) {
}
/**
* Return a unique identifier for the item at the given position.
*
* <p>The default implementation returns the given position.
* Subclasses should override this method if the positions of items can change.</p>
*
* @param position Position within this adapter
* @return Unique identifier for the item at position
*/
public long getItemId(int position) {
return position;
}
/**
* 可以自定义fragmentName
*/
protected String makeFragmentName(int viewId, int position) {
return "android:switcher:" + viewId + ":" + position;
}
/**
* Fragment的LRU缓存
*/
private class FragmentCache extends LruCache<String, Fragment> {
public FragmentCache(int maxSize) {
super(maxSize);
}
@Override
protected void entryRemoved(boolean evicted, String key, Fragment oldValue, Fragment newValue) {
if (evicted || (newValue != null && oldValue != newValue)) {
if (mCurTransaction != null) {
mCurTransaction.remove(oldValue);
}
}
}
}
}