RecyclerView嵌套ViewPager引发的问题

11/7/2021 ViewPager

# 事件背景

由于应用首页需要整体滑动,使用了RecyclerView中嵌套ViewPager的方式,但测试发现应用被杀死后ViewPager中的数据无法恢复,功能区显示空白。

# 事件分析

因为替换为FragmentStatePagerAdapter后恢复正常,我着重对比了设置不同adapter时的状态。 先用开发者工具确定了FragmentPagerAdapter中Fragment的View未被添加到ViewPager中,而FragmentStatePagerAdapter可以添加的区别,那么首先跟踪系统恢复的流程到

# FragmentStateManager.java
void createView() {
  ...
  if (container != null) {
      addViewToContainer();
  }

发现不管设置哪个Adapter,在数据恢复期间container都为空,也就是说系统不能根据Fragment之前保存的mContainerId找到它的父容器。

那为什么FragmentStatePagerAdapter又能够正常显示呢?是否是因为setAdapter后重新进行了addView?

接下来继续跟踪setAdapter后FragmentManager的变化,根据debug时的堆栈信息,我们把断点放在FragmentManager的一个重要的方法executeOpsTogether上,

# FragmentManager.java
private void executeOpsTogether(@NonNull ArrayList<BackStackRecord> records,
        @NonNull ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {
            ...
  for (FragmentTransaction.Op op : record.mOps) {
    Fragment fragment = op.mFragment;
    if (fragment != null) {
        FragmentStateManager fragmentStateManager = createOrGetFragmentStateManager(fragment);
        // 这个方法中根据生命周期Fragment.VIEW_CREATED调用了上面的createView()
        fragmentStateManager.moveToExpectedState();
    }
  }

在这个方法中确定调用上面createView前Fragment的是否还是系统恢复时的Fragment,经过调试比对发现Fragment是一个新的对象(此处我排查时先比对的State绕了一下)。

而该新对象正是Adapter初始化时传入的对象(此处为了简单每次创建view是都初始化了fragment),FragmentStatePagerAdapter没有像FragmentPagerAdapter一样先从FragmentManager中找,而直接使用了getItem返回的对象,

# FragmentStatePagerAdapter.java

public Object instantiateItem(@NonNull ViewGroup container, int position) {
    // 这里的判断逻辑未能进入
    if (mFragments.size() > position) {
        Fragment f = mFragments.get(position);
        if (f != null) {
            return f;
        }
    }
    ...
    // 继续走到了这里
    Fragment fragment = getItem(position);

正是由于使用的新对象触发了生命周期的变化进行了重新绑定,进而保证了ViewPager能正常添加Fragment的View。

但FragmentStatePagerAdapter中为什么没有回调saveState和restoreState来避免重新创建对象呢?

# FragmentStatePagerAdapter.java

@Override
public void restoreState(@Nullable Parcelable state, @Nullable ClassLoader loader) {
    if (state != null) {
        ...
        // 正常恢复流程下FragmentStatePagerAdapter会走这里
        for (String key: keys) {
            if (key.startsWith("f")) {
                int index = Integer.parseInt(key.substring(1));
                Fragment f = mFragmentManager.getFragment(bundle, key);
                if (f != null) {
                    while (mFragments.size() <= index) {
                        mFragments.add(null);
                    }
                    f.setMenuVisibility(false);
                    mFragments.set(index, f);
                } else {
                    Log.w(TAG, "Bad fragment at key " + key);
                }
            }
        }
    }
}

接着跟踪ViewPager的onSaveInstanceState方法,发现activity退到后台后未正常回调

# ViewPager.java
@Override
public Parcelable onSaveInstanceState() {
    Parcelable superState = super.onSaveInstanceState();
    SavedState ss = new SavedState(superState);
    ss.position = mCurItem;
    if (mAdapter != null) {
        // 这里会调用adapter的saveState保存状态以保证数据能够正常恢复
        ss.adapterState = mAdapter.saveState();
    }
    return ss;
}

onSaveInstanceState又是从activity的viewtree逐级调用的,来到RecyclerView的onSaveInstanceState方法中

# RecyclerView.java
@Override
protected Parcelable onSaveInstanceState() {
    SavedState state = new SavedState(super.onSaveInstanceState());
    if (mPendingSavedState != null) {
        state.copyFrom(mPendingSavedState);
    } else if (mLayout != null) {
        // 这里用了layoutManager去实现
        state.mLayoutState = mLayout.onSaveInstanceState();
    } else {
        state.mLayoutState = null;
    }

    return state;
}
# LinearLayoutManager.java
@Override
public Parcelable onSaveInstanceState() {
    // 这里为空
    if (mPendingSavedState != null) {
        return new SavedState(mPendingSavedState);
    }
    SavedState state = new SavedState();
    if (getChildCount() > 0) {
        //确保layoutState不为空
        ensureLayoutState();
        boolean didLayoutFromEnd = mLastStackFromEnd ^ mShouldReverseLayout;
        state.mAnchorLayoutFromEnd = didLayoutFromEnd;
        // 这里看到只是保存了列表能显示的第一条的adapter position和偏移量,未调用childView的onSaveInstaceState进行状态保存
        if (didLayoutFromEnd) {
            final View refChild = getChildClosestToEnd();
            state.mAnchorOffset = mOrientationHelper.getEndAfterPadding()
                    - mOrientationHelper.getDecoratedEnd(refChild);
            state.mAnchorPosition = getPosition(refChild);
        } else {
            final View refChild = getChildClosestToStart();
            state.mAnchorPosition = getPosition(refChild);
            state.mAnchorOffset = mOrientationHelper.getDecoratedStart(refChild)
                    - mOrientationHelper.getStartAfterPadding();
        }
    } else {
        state.invalidateAnchor();
    }
    return state;
}

那么即使最后回调了onRestoreInstanceState也不会有数据(当然这里实际上也不会回调)

而FragmentPagerAdapter初始化Fragment数据时是从mFragmentManager.findFragmentByTag去查找的,肯定是能找到的

# FragmentPagerAdapter.java
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }

    final long itemId = getItemId(position);

    // Do we already have this fragment?
    String name = makeFragmentName(container.getId(), itemId);
    //这里的fragment是activity恢复时创建的,但它虽然被创建了,却未能添加到ViewPager中,
    //且此时它的mState已被设置为RESUMED,无法进行重新绑定,所以无法正常显示
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);
    } else {
        // 不会走到这里了
        fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }
    ...
    return fragment;
}

# 总结

虽然使用了FragmentStatePagerAdapter表面上解决了问题,却让fragment重新创建了一次并重新走了一遍它生命周期,未能做到重用。所以这类嵌套了Fragment的视图,我们应该尽量避免将它们放在RecyclerView中,或者我们也可以仿照FragmentStateAdapter来改造FragmentPagerAdapter,或者直接将ViewPager替换为ViewPager2再直接配合FragmentStateAdapter使用。

# FragmentStateAdapter.java
private void scheduleViewAttach(final Fragment fragment, @NonNull final FrameLayout container) {
    // 监听生命周期变化,当fragment已经创建时直接添加到容器中
    mFragmentManager.registerFragmentLifecycleCallbacks(
            new FragmentManager.FragmentLifecycleCallbacks() {
                @Override
                public void onFragmentViewCreated(@NonNull FragmentManager fm,
                        @NonNull Fragment f, @NonNull View v,
                        @Nullable Bundle savedInstanceState) {
                    if (f == fragment) {
                        fm.unregisterFragmentLifecycleCallbacks(this);
                        addViewToContainer(v, container);
                    }
                }
            }, false);
}