Android ViewPager2 + FragmentStateAdapter 的使用以及问题

场景介绍:在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 的场景,特别是数据动态变化或数据量较大的情况。该适配器的最简单使用方式如下:

java 复制代码
        adapter = new FragmentStateAdapter(getChildFragmentManager(), getLifecycle()) {
            @NonNull
            @Override
            public Fragment createFragment(int position) {
                return fragments.get(position);
            }

            @Override
            public int getItemCount() {
                return fragments.size();
            }
        };

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)方法。很遗憾,该方法并不能解决问题,该方法的源码如下:

java 复制代码
    public void setAdapter(@Nullable @SuppressWarnings("rawtypes") Adapter adapter) {
        final Adapter<?> currentAdapter = mRecyclerView.getAdapter();
        mAccessibilityProvider.onDetachAdapter(currentAdapter);
        unregisterCurrentItemDataSetTracker(currentAdapter);
        mRecyclerView.setAdapter(adapter);
        mCurrentItem = 0;
        restorePendingState();
        mAccessibilityProvider.onAttachAdapter(adapter);
        registerCurrentItemDataSetTracker(adapter);
    }

在该方法执行的过程中restorePendingState的源码如下:

java 复制代码
 private void restorePendingState() {
        if (mPendingCurrentItem == NO_POSITION) {
            // No state to restore, or state is already restored
            return;
        }
        Adapter<?> adapter = getAdapter();
        if (adapter == null) {
            return;
        }
        if (mPendingAdapterState != null) {
            if (adapter instanceof StatefulAdapter) {
                ((StatefulAdapter) adapter).restoreState(mPendingAdapterState);
            }
            mPendingAdapterState = null;
        }
        // Now we have an adapter, we can clamp the pending current item and set it
        mCurrentItem = Math.max(0, Math.min(mPendingCurrentItem, adapter.getItemCount() - 1));
        mPendingCurrentItem = NO_POSITION;
        mRecyclerView.scrollToPosition(mCurrentItem);
        mAccessibilityProvider.onRestorePendingState();
    }
	/**
	restoreState方法如下
	**/
    @Override
    public final void restoreState(@NonNull Parcelable savedState) {
        for (String key : bundle.keySet()) {
            if (isValidKey(key, KEY_PREFIX_FRAGMENT)) {
                long itemId = parseIdFromKey(key, KEY_PREFIX_FRAGMENT);
                Fragment fragment = mFragmentManager.getFragment(bundle, key);
                mFragments.put(itemId, fragment);
                continue;
            }

            if (isValidKey(key, KEY_PREFIX_STATE)) {
                long itemId = parseIdFromKey(key, KEY_PREFIX_STATE);
                Fragment.SavedState state = bundle.getParcelable(key);
                if (containsItem(itemId)) {
                    mSavedStates.put(itemId, state);
                }
                continue;
            }

            throw new IllegalArgumentException("Unexpected key in savedState: " + key);
        }
    }

需要注意mPendingAdapterState 这一变量,该变量将保留了历史fragment的基本信息,因此在setAdapter的方法过程中还会将viewPage2的一些信息设置到你新的adapter中,是不是很尴尬?setAdapter方法并不是简单的把adapter方法设置后就结束了,viewPage2内部还将自己历史关心的数据设置到该adapter中!

FragmentStateAdapter 的createFragment源码如下:

java 复制代码
    private void ensureFragment(int position) {
        long itemId = getItemId(position);
        if (!mFragments.containsKey(itemId)) {
            // TODO(133419201): check if a Fragment provided here is a new Fragment
            Fragment newFragment = createFragment(position);
            newFragment.setInitialSavedState(mSavedStates.get(itemId));
            mFragments.put(itemId, newFragment);
        }
    }

mFragments对象对于adapter很重要,该对象持有了历史创建的fragment,这样就导致无需每次使用的过程中进行重复创建了,但这会导致一个尴尬的问题,该mFragments默认查找是按照位置进行查找的,换句话说一旦viewPage2完成展示以及数据加载,在后续的切换过程中,就算你用createFragment可以创建fragment对象,但是由于相同位置下mFragments中已经存在数据,所以根部不会执行createFragment方法!

至此闭环:setAdapter方法会使用到viewPage2持有的savedState设置adapter的mFragments对象,ensureFragment方法会根据mFragments按照position判断fragment是否存在!到此结束。

总结

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

  • viewPage2销毁fragment后依旧会持有其对象信息,并标记在adapter中的mFragments中,在后续复现时不会再进行对象的创建
  • 将Binding设置成null并不一定会导致viewPage2的重绘,其依旧可能保留自己原始数据。
  • viewPape2在进行setAdapter方法的过程中会将自己持有的fragment对象标记信息设置到FragmentStateAdapter 的mFragments中。
相关推荐
lhbian2 小时前
PHP、C++和C语言对比:哪个更适合你?
android·数据库·spring boot·mysql·kafka
catoop3 小时前
Android 最佳实践、分层架构与全流程解析(2025)
android
ZHANG13HAO3 小时前
Android 13 特权应用(Android Studio 开发)调用 AOSP 隐藏 API 完整教程
android·ide·android studio
田梓燊4 小时前
leetcode 142
android·java·leetcode
angerdream4 小时前
Android手把手编写儿童手机远程监控App之JAVA基础
android
菠萝地亚狂想曲4 小时前
Zephyr_01, environment
android·java·javascript
sTone873755 小时前
跨端框架通信机制全解析:从 URL Schema 到 JSI 到 Platform Channel
android·前端
sTone873755 小时前
Java 注解完全指南:从 "这是什么" 到 "自己写一个"
android·前端
catoop5 小时前
Kotlin 协程在 Android 开发中的应用:定义、优势与对比
android·kotlin
撒旦物种5 小时前
Android WebView 获取内容高度
android·webview