应用重建导致 Viewpager 中 Fragment 显示异常

614 阅读2分钟

简述

同事开发过程中,由于需要用到了 Viewpager 组件很简单也没有什么复杂的东西,就使用 Viewpager 加载了几个 FragmentFragment 中有个 RecyclerView 里面加载了一些 item,大概就长这么个样子(如下图)。 image.png

发现问题

测试在测试过程中,修改了虚拟导航的方式,从手势变成了按键式。然后回到应用,发现 Fragment 中数据丢失了,并且必现。(同事过节跑了,只能转到我手上了)

先看下代码吧

TestKotlinApiActivity.kt
class TestKotlinApiFragment : SupportFragment() {
    、、、、
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        vpBrowser.run {
            adapter = TestAdapter(childFragmentManager, arrayListOf("", "", "", ""))
        }
    }
}
TestAdapter.kt
class TestAdapter(fm: FragmentManager, val datas: MutableList<String>) :
    FragmentStatePagerAdapter(fm) {
    private val mFragments = hashMapOf<Int, Fragment>()
    override fun getCount(): Int {
        return datas.size
    }

    override fun getItem(position: Int): Fragment {
        return mFragments.getOrPut(position) {
            Fragment().apply { 
                setData(datas)
            }
        }
    }
}

查找问题原因

发现系统修改虚拟会导致 Fragment 重建。从日志中分析所有页面都重建了,数据也给到了新的 Fragment ,但是显示出来的 Fragment 就是一个空的,而且没有调用 onStart、onCreateView 等等方法只调用了 onResume ,这里就是一个比较奇怪的点了?

这里就有两个奇怪的点:

  1. 为什么 Fragment 不走 onstart、onCreateView、onCreatedView 方法,单单走了 onResume?
  2. 为什么给了数据,列表还是显示为空呢?

第二点比较好解释,由于 Fragment 不走 onstart、onCreateView、onViewCreated 方法导致 adpter 为空了

第一点就比较奇怪了,就算是重建 TestKotlinApiFragment 的生命周期都走了,而且 TestAdapter 也重建了,并且该给的数据也给了子 Fragment 为什么数据显示为空?这个时候单看这几个类已经解释不了了,只能去父类看看源码了

// FragmentStatePagerAdapter.class 部分源码

private ArrayList<Fragment> mFragments = new ArrayList<>();

@NonNull
public abstract Fragment getItem(int position);

@NonNull
@Override
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);
    // 省略部分代码
    while (mFragments.size() <= position) {
        mFragments.add(null);
    }
    // 省略部分代码
    return fragment;
}

@Override
@Nullable
public Parcelable saveState() {
    Bundle state = null;
   if (mSavedState.size() > 0) {
        state = new Bundle();
        Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
        mSavedState.toArray(fss);
        state.putParcelableArray("states", fss);
    }
    for (int i=0; i<mFragments.size(); i++) {
        Fragment f = mFragments.get(i);
        if (f != null && f.isAdded()) {
            if (state == null) {
                state = new Bundle();
            }
            String key = "f" + i;
            mFragmentManager.putFragment(state, key, f);
        }
   }
    return state;
}
    
@Override
public void restoreState(@Nullable Parcelable state, @Nullable ClassLoader loader) {
    if (state != null) {
        Bundle bundle = (Bundle)state;
        bundle.setClassLoader(loader);
        Parcelable[] fss = bundle.getParcelableArray("states");
        mSavedState.clear();
        mFragments.clear();
        if (fss != null) {
            for (int i=0; i<fss.length; i++) {
                mSavedState.add((Fragment.SavedState)fss[i]);
            }
        }
        Iterable<String> keys = bundle.keySet();
        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);
                }
            }
        }
    }
}

FragmentStatePagerAdapter 源码中其实立马就能找出为什么子 Fragmet 为什么为空了

// FragmentStatePagerAdapter.class 部分源码
@NonNull
@Override
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);
    // 省略部分代码
    while (mFragments.size() <= position) {
        mFragments.add(null);
    }
    // 省略部分代码
    return fragment;
}

从这段代码可以看出,由于刚开始创建 mFragments 肯定是一个空的列表,那么就会调用 getItem 去获取 Fragment 填充到内部维护的 Fragmet 列表中。这个时候由于修改了虚拟键盘,导致应用重建就会调用 saveState 方法,将 mFragmentsFragments 存放到 FragmentManager 中。下面是源码

// FragmentStatePagerAdapter.class 部分源码
@Override
@Nullable
public Parcelable saveState() {
    Bundle state = null;
   if (mSavedState.size() > 0) {
        state = new Bundle();
        Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
        mSavedState.toArray(fss);
        state.putParcelableArray("states", fss);
    }
    for (int i=0; i<mFragments.size(); i++) {
        Fragment f = mFragments.get(i);
        if (f != null && f.isAdded()) {
            if (state == null) {
                state = new Bundle();
            }
            String key = "f" + i;
            mFragmentManager.putFragment(state, key, f);
        }
   }
    return state;
}

重建的时候又会调用 restoreState 方法恢复 Fragment。源码如下

// FragmentStatePagerAdapter.class 部分源码
@Override
public void restoreState(@Nullable Parcelable state, @Nullable ClassLoader loader) {
    if (state != null) {
        Bundle bundle = (Bundle)state;
        bundle.setClassLoader(loader);
        Parcelable[] fss = bundle.getParcelableArray("states");
        mSavedState.clear();
        mFragments.clear();
        if (fss != null) {
            for (int i=0; i<fss.length; i++) {
                mSavedState.add((Fragment.SavedState)fss[i]);
            }
        }
        Iterable<String> keys = bundle.keySet();
        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);
                }
            }
        }
    }
}

从源码中可以看到,恢复的时候首先会将 mFragments.clear() 掉,然后再从 FragmentManager 中获取到之前存储的 Fragment 填充到 mFragents 中。所以这个时候无论是 Adapter 是否是新的,最终还是用的销毁之前存储的 Fragment

这个时候就会导致外部自己维护的 mFragents 和内部的 mFragents 不一致。而且源码中内部维护的 mFragents 不为空就直接返回了,所以无论你怎么更新自己维护的 mFragents 都没法去显示到屏幕上。

// FragmentStatePagerAdapter.class 部分源码
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
    if (mFragments.size() > position) {
        Fragment f = mFragments.get(position);
        if (f != null) {
            return f;
        }
    }
    // 省略部分代码
    return fragment;
}

解决方案

很简单,既然知道外部的 Fragments 和 内部的 Fragments 不一致,那么就想办法统一嘛。重写 restoreState 方法

override fun restoreState(state: Parcelable?, loader: ClassLoader?) {
    if (state != null) {
        val bundle = state as Bundle
        bundle.classLoader = loader
        mFragments.clear()
        val keys: Iterable<String> = bundle.keySet()
        for (key in keys) {
            if (key.startsWith("f")) {
                val index = key.substring(1).toInt()
                val f = fm.getFragment(bundle, key)
                if (f != null) {
                    f.setMenuVisibility(false)
                    mFragments[index] = f
                }
            }
        }
    }
}

直接照搬源码自己也用存储的 Fragments 这样就能完美运行起来了。

好,到这里 bug 就解决了,当然有更好的方案大佬们可以指点指点。