踩坑之FragmentPagerAdapter

1,260 阅读5分钟

最近做需求的时候,遇到ViewPager+FragmentPagerAdapter场景下面Fragment复用的问题。甚至我重新初始化Adapter,重新设置Adapter还是没有作用。

背景介绍

b050f683-7822-4f54-bb83-942b898e06eb未命名文件.webp

我们主页的结构如上图,一个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;
}

我们可以看到几个重要信息

  1. 缓存在FragmentManger中,每次取Item的时候,从FragmentManager 的缓存中找,没有找到才调用 getItem(position) 获取对应Fragment
  2. 缓存的key 为 父容器id + getItemId 默认实现返回position
  3. 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 两个问题

  1. FragmentPagerAdapter与position 绑定,而不是与数据绑定
  2. FragmentPagerAdapter的Fragment无法删除

解决方案:

  1. 切到到ViewPager2+FragmentStateAdapter中(它调用的remove方法)
  2. 修改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);
                }
            }
        }
    }
}