1.问题描述
如上图所示,布局为典型的Activity嵌套ViewPager再嵌套Fragment
在首次进入该页面和正常使用时没有任何问题,但是在Activity意外销毁时会出现空指针问题(也不一定是这个问题,后面会详细说明)造成崩溃
2.原因分析
- 首先,正常使用没有问题则说明代码逻辑上没有太大的硬伤
- 出现意外销毁时导致,则问题最有可能出现在保存和恢复状态期间
kotlin.UninitializedPropertyAccessException: lateinit property mViewModel has not been initialized
具体的报错信息为上面的红字,这个是kotlin的lateinit属性在调用时没有初始化的Exception,可以理解为Java的空指针
发生在ViewPager.OnPageChangeListener.onPageSelected(),在这个回调中,调用了Fragment中的刷新方法,导致了崩溃
注释掉该行代码后,没有再次崩溃,而且Fragment可以正常使用(如刷新,点击等)
这就说明,Activity正常恢复,Fragment正常恢复,问题出现在ViewPager
在Activity中,使用了一个List来保存这些Fragment,在刚才的回调中会使用该list获取对应的Fragment进行操作
这时候就会有人说:list肯定没更新
但是往下看,在Activity重建时,同样也会调用到onCreate,那么在这个流程中该list与fragment都进行了重建,而且为ViewPager的Adapter进行了重新赋值
按一般思维来说,怎么着都不应该出问题,这些Fragment,Adapter啥的都是新的
但是看一下关于ViewPager恢复相关的代码
//ViewPager
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();//这里的super是View,就是一般的现场保存
SavedState ss = new SavedState(superState);
ss.position = mCurItem;
if (mAdapter != null) {
ss.adapterState = mAdapter.saveState();
}
return ss;
}
//PagerAdapter
@Nullable
public Parcelable saveState() {
return null;
}
//FragmentPagerAdapter
@Override
public Parcelable saveState() {
return null;
}
//FragmentStatePagerAdapter,相比于FragmentPagerAdapter增加了对Fragment的状态保存
@Override
public Parcelable saveState() {
Bundle state = null;
...
for (int i=0; i<mFragments.size(); i++) {
Fragment f = mFragments.get(i);
if (f != null && f.isAdded()) {
...
String key = "f" + i;
mFragmentManager.putFragment(state, key, f);
}
}
return state;
}
这段代码可以看出来,FragmentPagerAdapter没有对状态进行保存,而FragmentStatePagerAdapter将Fragment的状态保存到了FragmentManager中
而在创建Adapter时,是通过重写 getItem(int position); 方法进行Fragment的创建(普通ViewPager是通过instantiateItem(ViewGroup container, int position) 进行创建)
而通过源码可以知道,getItem同样也是在instantiateItem方法中进行的调用,如下(省略了无关代码)
//FragmentPagerAdapter,没有保存状态,所以也没有恢复状态
public Object instantiateItem(@NonNull ViewGroup container, int position) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
final long itemId = getItemId(position);
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);//1
if (fragment != null) {
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);//2
...
}
...
return fragment;
}
//FragmentStatePagerAdapter,恢复状态
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
if (state != null) {
Bundle bundle = (Bundle)state;
bundle.setClassLoader(loader);
...
mFragments.clear();
...
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) {
...
mFragments.set(index, f);
} ...
}
}
}
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
if (mFragments.size() > position) {//21
Fragment f = mFragments.get(position);
if (f != null) {
return f;
}
}
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
Fragment fragment = getItem(position);//22
...
return fragment;
}
3.解决办法
1.治标:设置configChanges的uiMode和screenSize等属性使Activity不在旋转屏幕和切换暗黑模式时重新构建,减少Activity的意外销毁(但是还是有可能在内存不足时进行销毁)
2.治本:既然FragmentManager管理了所有的Fragment,那么在使用时也通过该FragmentManager进行获取而不是通过list,如果想使用list那么应该在页面恢复时重新构建一个List,代码如下:
val fragments: List<Fragment>? = supportFragmentManager.fragments
val list = mutableListOf<OrderFragment>()
if (fragments == null || fragments.isEmpty()) {
list.add(XXXFragment.newFragment(ListTypeXXX))
list.add(XXXFragment.newFragment(ListTypeXXX))
list.add(XXXFragment.newFragment(ListTypeXXX))
list.add(XXXFragment.newFragment(ListTypeXXX))
list.add(XXXFragment.newFragment(ListTypeXXX))
} else {
for (fragment in fragments) {
if (fragment is OrderFragment) {
list.add(fragment)
}
}
}