code小生 一个专注大前端领域的技术平台公众号回复 Android 加入安卓技术群
作者:不会飞的小猪 链接: https://www.jianshu.com/p/4ce35bc086aa 声明:本文已获 不会飞的小猪 授权发表,转发等请联系原作者授权
一、问题描述
经常会碰到如下这样的页面架构:
imageTabLayout+ViewPager+FragmentStatePagerAdapter+Fragment实现起来很容易(本文以此作为案例分析),当App处于后台一段时间后(可能10分钟以后或者更多),再进入App时,Fragment显示区域就变成看空白。这种情况是被系统给回收掉了。
如何判定被系统回收了? 为什么显示不出内容? 解决方案二、解决问题
如何判定被系统回收了代码层面做相应的判断即可:判定adapter是否为null,不为null的情况下再判定fragment的状态:
/** * @return 是否fragment被系统给detach或者销毁了 */ public boolean isFragmentsDetachedOrDestroyed() { if (getCount() > 0 && fragmentList != null && fragmentList.size() > 0) { for (int i = 0; i < fragmentList.size(); i++) { if (fragmentList.get(i) == null || fragmentList.get(i).isDetached() || !fragmentList.get(i).isAdded()) { return true; } } } else { return true; } return false; }
只要判断其中一个不存在了即可。
当然,还有一种方法。我们知道,非用户正常退出的销毁时,系统会保存对应的数据调用onSaveInstanceState,并在页面再次展现时进行恢复调用onRestoreInstanceState,关于这两个方法,也可以在ViewPager源码中看到。
@Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.position = mCurItem; if (mAdapter != null) { ss.adapterState = mAdapter.saveState(); } return ss; } @Override public void onRestoreInstanceState(Parcelable state) { if (!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); if (mAdapter != null) { mAdapter.restoreState(ss.adapterState, ss.loader); setCurrentItemInternal(ss.position, false, true); } else { mRestoredCurItem = ss.position; mRestoredAdapterState = ss.adapterState; mRestoredClassLoader = ss.loader; } }
那么只要判断mRestoredCurItem、mRestoredAdapterState、mRestoredClassLoader不为默认值也可以达到目的。(此处不展开)
为什么显示不出内容?
在本文的案例中,因为需求需要,每次在判断被回收后,都会重新new Adapter来给ViewPager。
mAdapter = new CustomPagerAdapter(getActivity(), getChildFragmentManager(), list); viewPager.setAdapter(mAdapter); viewPager.setOffscreenPageLimit(size);
一般常理思考,重新new的adapter对象,应该会指向新的引用,那么应该会重新创建对应的Fragment,进而应该显示出来,但实际并没有。
来看看自定义的Adapter中的写法 (extends FragmentStatePagerAdapter):
@NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { LogUtil.d(TAG, "instantiateItem->" + position); return super.instantiateItem(container, position); } @Override public Fragment getItem(int position) { LogUtil.d(TAG, "getItem->" + position); return fragmentList.get(position); }
为什么没有生成新的Fragment?从打印的日志可以看到,instantiateItem有调用,但是getItem并没有调用,从这里就没有返回Fragment。 追踪FragmentStatePagerAdapter中的写法:
@NonNull public Object instantiateItem(@NonNull ViewGroup container, int position) { Fragment fragment; if (this.mFragments.size() > position) { fragment = (Fragment)this.mFragments.get(position); if (fragment != null) { return fragment; } } if (this.mCurTransaction == null) { this.mCurTransaction = this.mFragmentManager.beginTransaction(); } fragment = this.getItem(position); if (this.mSavedState.size() > position) { SavedState fss = (SavedState)this.mSavedState.get(position); if (fss != null) { fragment.setInitialSavedState(fss); } } while(this.mFragments.size() <= position) { this.mFragments.add((Object)null); } fragment.setMenuVisibility(false); fragment.setUserVisibleHint(false); this.mFragments.set(position, fragment); this.mCurTransaction.add(container.getId(), fragment); return fragment; }
可以看到,当mFragments中能找到对应position的Fragment时,则不会去调用getItem方法而使用FragmentStatePagerAdapter中保有的属性值。那么为什么新new 的adapter这个值仍然会不为空呢?追踪下它的赋值:
public void restoreState(Parcelable state, ClassLoader loader) { if (state != null) { Bundle bundle = (Bundle)state; bundle.setClassLoader(loader); Parcelable[] fss = bundle.getParcelableArray("states"); this.mSavedState.clear(); this.mFragments.clear(); if (fss != null) { for(int i = 0; i < fss.length; ++i) { this.mSavedState.add((SavedState)fss[i]); } } Iterable<String> keys = bundle.keySet(); Iterator var6 = keys.iterator(); while(true) { while(true) { String key; do { if (!var6.hasNext()) { return; } key = (String)var6.next(); } while(!key.startsWith("f")); int index = Integer.parseInt(key.substring(1)); Fragment f = this.mFragmentManager.getFragment(bundle, key); if (f != null) { while(this.mFragments.size() <= index) { this.mFragments.add((Object)null); } f.setMenuVisibility(false); this.mFragments.set(index, f); } else { Log.w("FragmentStatePagerAdapt", "Bad fragment at key " + key); } } } } }
系统销毁时会调用以上方法,可以看到这样一句 Fragment f = this.mFragmentManager.getFragment(bundle, key); 是通过FragmentManager中拿到的Fragment,进而加入到了adapter中的mFragments中。而整个restoreState的调用有两处:
viewPager.setAdapter viewPager.onRestoreInstanceState因为每次新生成的adapter的FragmentManager都是同一对象(这个也无法改为其它的),所以在setAdapter时,仍旧会将同一FragmentManager中之前保存的Fragment列表值赋值给新生成的adapter中的mFragments,进而在instantiateItem中用mFragments做判断时,它是有值的。从而也就不会去调用getItem方法了。
解决方案
有很多解决方法,以下提供两种; ①改写FragmentStatePagerAdapter,注释掉从mFragments中拿Fragment返回的代码块。
@NonNull public Object instantiateItem(@NonNull ViewGroup container, int position) { Fragment fragment; // if (this.mFragments.size() > position) { // fragment = (Fragment)this.mFragments.get(position); // if (fragment != null) { // return fragment; // } // } //以下代码省略 }
②自定义的Adapter中的instantiateItem方法中清除掉mFragments等数据
@NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { LogUtil.d(TAG, "instantiateItem->" + position); try { Field mFragments = getClass().getSuperclass().getDeclaredField("mFragments"); mFragments.setAccessible(true); ((ArrayList) mFragments.get(this)).clear(); Field mSavedState = getClass().getSuperclass().getDeclaredField("mSavedState"); mSavedState.setAccessible(true); ((ArrayList) mSavedState.get(this)).clear(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return super.instantiateItem(container, position); }
针对本文提到的情况,也还有一些其它的解决方案,例如:直接拿系统保存的值来重新赋值也可以。如果使用FragmentPagerAdapter,解决方案也是类似的思路。
查看更多关于Android Fragment 被回收后显示空白问题解决方案的详细内容...