Android ViewPager2 + FragmentStateAdapter 的利用以及问题

打印 上一主题 下一主题

主题 977|帖子 977|积分 2946

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
  场景介绍:在Android业务功能开发的过程中,需要利用到嵌套ViewPage2实现页面切换,这种场景在我们的开发过程中并不少见,大致结构为一个activity包含一个viewPage2,这个viewPage2中存在一个fragment A,fragment A中也包含了一个viewPage2。全部viewPage2都利用FragmentStateAdapter 适配器实现界面数据联动。
  上述实现过程并不复杂,但是在我实际业务中需要实现activity调用fragmentA中viewPage2的一些方法,固然这个需求可以利用viewModel进行实现,但是由于初版利用了方法调用,碰到了bug所以针对该功能的实现进行初步研究。
  FragmentStateAdapter 介绍及简单利用

  FragmentStateAdapter 是 Android Jetpack 中提供的用于管理 Fragment 的适配器,它是 RecyclerView.Adapter 的子类。
  FragmentStateAdapter 会在 ViewPager 中显示的每个 Fragment 的生命周期之间进行恰当的生存和恢复 Fragment 的状态,以确保内存占用较小。
  当 Fragment 不再可见时,FragmentStateAdapter 会销毁该 Fragment 的视图,但会保存其实例状态,以便在需要时重新创建。
  适用于大量 Fragment 的场景,特别是数据动态变革或数据量较大的情况。该适配器的最简单利用方式如下:
  1.         adapter = new FragmentStateAdapter(getChildFragmentManager(), getLifecycle()) {
  2.             @NonNull
  3.             @Override
  4.             public Fragment createFragment(int position) {
  5.                 return fragments.get(position);
  6.             }
  7.             @Override
  8.             public int getItemCount() {
  9.                 return fragments.size();
  10.             }
  11.         };
复制代码
fragment切换销毁

  在默认情况下,viewPage2提供的性能优化实现了临近一个fragment预加载机制,及假如初始展示第0个fragment,viewPage2也会把第1个fragment进行创建视图但并不展示。也就是说viewPage2默认的缓存机制会缓存三个fragment,一旦需要缓存的实例凌驾三个,例如从第0个滑动到第2个,则会缓存123位置的fragment,响应的第0个fragment将被销毁,不停执行到onDestroy()生命周期。
  值得说明的是:销毁仅仅代表了生命周期的结束,默认情况下该fragment的实例、其内部成员变量以及其绑定的视图都不肯定会消散。 基于这一原因,为了防止内存溢出我们在onDestroy()生命周期一样寻常会针对成员变量进行setNull操纵。通过setNull可以将成员变量消除引用,以便触发GC。打仗过java都清楚即便没有引用的变量也未必里面会触发GC,因此当我门将Binding设置成null后,其关联的view也未必会里面消散,在fragment在此展示时,依旧有大概调用上次绘制过的view进行显示。而且在通过viewPage2切换导致fragment销毁的过程中,其本质上是执行到了onDestroy()生命周期,并不见得会销毁视图,而且viewPage2还将生存一个该fragment的实例!根据上述内容可以总结下面几点:


  • 进入onDestroy生命周期并不能肯定是成员变量销毁。
  • 通过viewPage2切换导致fragment销毁本质上是让fragment执行到onDestroy()生命周期,但是viewPage2还生存了该fragment的实例
  • 假如在onDestroy()生命周期还没有把该fragment成员变量setNull,则viewPage2所持有的该fragment对象依旧保存着这些fragment成员变量
  • 在onDestroy()生命周期中将Binding设置成null后并不能将其view都进行清空
fragment展示

 展示通常有三种,一种是viewPage2内缓存的fragment复现,一种是新的未展示过的fragment展示,尚有一种是被销毁了的fragment的展示:他们对应一下过程:


  • 缓存内fragment展示:执行onResume()后直接进行展示
  • 未展示过的fragment展示:调用构造方法初始化实例 – 调用onCreate不停执行到onResume生命周期
  • 销毁的fragment重新展示:调用onCreateView不停执行到onResume进行展示。
  需要留意的是销毁的fragment重新展示的过程中并没有进行fragment实例创建,因此本质上viewPage2已经拥有该实例了,知识当时调用了onDestroy方法而已。
我的问题

  在我的业务场景中,需要利用到viewPage2下的fragment实例,然后调用该实例的方法,假如只是单层viewPage2的利用,则相对比力简单,但是假如是嵌套viewPage2则会出现以下问题:
  一旦持有viewPage2的fragment,在其所属的viewPage2切换过程中销毁了,然后又由销毁状态到复现,此时通过上述FragmentStateAdapter设置的fragment回调会导致非常。
 在适配器的实现过程中,我们通过fragments【list】进行fragment对象持有,假如fragmentA【持有viewPage2的那个fragment】被复现时,假如我们在oncreateView生命周期对fragments进行初始化,调用add(fragment)方法,那么此时复现导致fragments持有对象和上次展示时其所持有对象差别!在fragmentA复现过程中必然也进行着fragmentA所持有的viewPage2下的fragment复现,刚才已经说了销毁的复现本质上是oncreate生命周期的重新调用,此时调用的是原来持有fragment对象的oncreate生命周期,而在fragmentA复现的过程中导致fragments持有的对象和汗青对象差别,这些对象严酷来讲仅仅经历了对象实例化阶段,未执行fragment的其他生命周期,还未创建持有视图,假如我们调用视图的干系操纵则会导致空指针等非常情况!
 简单来说就是fragmentA的销毁并不会导致其持有的viewPage2的销毁,更不会导致viewPage2所持有的fragment的销毁,假如我们对fragments进行重新设置,此时创建的fragment对象仅仅创建对象而已。
viewPage2的setAdapter

 按照我的问题描述,那么是不是我将viewPage2原先持有的fragment对象全都删除就能解决问题,删除的途径是调用viewPage2的setAdapter(null)方法。很遗憾,该方法并不能解决问题,该方法的源码如下:
  1.     public void setAdapter(@Nullable @SuppressWarnings("rawtypes") Adapter adapter) {
  2.         final Adapter<?> currentAdapter = mRecyclerView.getAdapter();
  3.         mAccessibilityProvider.onDetachAdapter(currentAdapter);
  4.         unregisterCurrentItemDataSetTracker(currentAdapter);
  5.         mRecyclerView.setAdapter(adapter);
  6.         mCurrentItem = 0;
  7.         restorePendingState();
  8.         mAccessibilityProvider.onAttachAdapter(adapter);
  9.         registerCurrentItemDataSetTracker(adapter);
  10.     }
复制代码
在该方法执行的过程中restorePendingState的源码如下:
  1. private void restorePendingState() {
  2.         if (mPendingCurrentItem == NO_POSITION) {
  3.             // No state to restore, or state is already restored
  4.             return;
  5.         }
  6.         Adapter<?> adapter = getAdapter();
  7.         if (adapter == null) {
  8.             return;
  9.         }
  10.         if (mPendingAdapterState != null) {
  11.             if (adapter instanceof StatefulAdapter) {
  12.                 ((StatefulAdapter) adapter).restoreState(mPendingAdapterState);
  13.             }
  14.             mPendingAdapterState = null;
  15.         }
  16.         // Now we have an adapter, we can clamp the pending current item and set it
  17.         mCurrentItem = Math.max(0, Math.min(mPendingCurrentItem, adapter.getItemCount() - 1));
  18.         mPendingCurrentItem = NO_POSITION;
  19.         mRecyclerView.scrollToPosition(mCurrentItem);
  20.         mAccessibilityProvider.onRestorePendingState();
  21.     }
  22.         /**
  23.         restoreState方法如下
  24.         **/
  25.     @Override
  26.     public final void restoreState(@NonNull Parcelable savedState) {
  27.         for (String key : bundle.keySet()) {
  28.             if (isValidKey(key, KEY_PREFIX_FRAGMENT)) {
  29.                 long itemId = parseIdFromKey(key, KEY_PREFIX_FRAGMENT);
  30.                 Fragment fragment = mFragmentManager.getFragment(bundle, key);
  31.                 mFragments.put(itemId, fragment);
  32.                 continue;
  33.             }
  34.             if (isValidKey(key, KEY_PREFIX_STATE)) {
  35.                 long itemId = parseIdFromKey(key, KEY_PREFIX_STATE);
  36.                 Fragment.SavedState state = bundle.getParcelable(key);
  37.                 if (containsItem(itemId)) {
  38.                     mSavedStates.put(itemId, state);
  39.                 }
  40.                 continue;
  41.             }
  42.             throw new IllegalArgumentException("Unexpected key in savedState: " + key);
  43.         }
  44.     }
复制代码
需要留意mPendingAdapterState 这一变量,该变量将保存了汗青fragment的根本信息,因此在setAdapter的方法过程中还会将viewPage2的一些信息设置到你新的adapter中,是不是很尴尬?setAdapter方法并不是简单的把adapter方法设置后就结束了,viewPage2内部还将本身汗青关心的数据设置到该adapter中!
FragmentStateAdapter 的createFragment源码如下:
  1.     private void ensureFragment(int position) {
  2.         long itemId = getItemId(position);
  3.         if (!mFragments.containsKey(itemId)) {
  4.             // TODO(133419201): check if a Fragment provided here is a new Fragment
  5.             Fragment newFragment = createFragment(position);
  6.             newFragment.setInitialSavedState(mSavedStates.get(itemId));
  7.             mFragments.put(itemId, newFragment);
  8.         }
  9.     }
复制代码
mFragments对象对于adapter很紧张,该对象持有了汗青创建的fragment,如许就导致无需每次利用的过程中进行重复创建了,但这会导致一个尴尬的问题,该mFragments默认查找是按照位置进行查找的,换句话说一旦viewPage2完成展示以及数据加载,在后续的切换过程中,就算你用createFragment可以创建fragment对象,但是由于相同位置下mFragments中已经存在数据,所以根部不会执行createFragment方法!
至此闭环:setAdapter方法会利用到viewPage2持有的savedState设置adapter的mFragments对象,ensureFragment方法会根据mFragments按照position判断fragment是否存在!到此结束。
总结

本文内容描述比力粗略,紧张讲述了viewPage2嵌套利用过程中的一些问题以及导致这些问题的原因,总结起来无非以下几点:


  • viewPage2销毁fragment后依旧会持有其对象信息,并标志在adapter中的mFragments中,在后续复现时会主动进行对象的创建,因此通过我们一开始传入的list无法获取最新的fragment
  • 将Binding设置成null并不肯定会导致viewPage2的重绘,其依旧大概保存本身原始数据。
  • viewPape2在进行setAdapter方法的过程中会将本身持有的fragment对象标志信息设置到FragmentStateAdapter 的mFragments中。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

大连全瓷种植牙齿制作中心

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表