saveEnabled导致的Fragment大量泄露

背景:

HomeActivity下有ViewPager2+FragmentStateAdapter+4个Fragment。这是一个很简单的页面。

在后台日志中发现,首页的Fragment一直在重复创建,最极端的日志中,一个Activity下有50多个Fragment

业务代码抽象后如下,Activity代码:

kotlin 复制代码
class LeakFragmentActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_fragment_leak)
        val adapter = ViewPagerAdapter(this)
        findViewById<ViewPager2>(R.id.viewPager).adapter = adapter
        adapter.refreshData(listOf("111", "222"))
    }
}

ViewPagerAdapter代码如下:

kotlin 复制代码
class ViewPagerAdapter(private val activity: FragmentActivity) : FragmentStateAdapter(activity) {

    private var pageTitles = emptyList<String>()

    override fun getItemCount(): Int = pageTitles.size

    override fun containsItem(itemId: Long): Boolean {
        return pageTitles.find { it.hashCode().toLong() == itemId } != null
    }

    override fun getItemId(position: Int): Long {
        return pageTitles[position].hashCode().toLong()
    }

    override fun createFragment(position: Int): Fragment {
        val result = PageFragment.newInstance(pageTitles[position]).apply {
            text = pageTitles[position]
        }

        val allFragments = activity.supportFragmentManager.fragments
        // 打印当前supportFragmentManager下的所有Fragment
        Log.i("FrankTest", "ViewPagerAdapter# createFragment size:${allFragments.size} allFragments:${allFragments.map { it.hashCode() }}")
        return result

    }

    fun refreshData(list: List<String>) {
        pageTitles = list
        notifyDataSetChanged()
    }
}

一、分析

尝试复现:

通过分析业务代码,以及多次尝试,终于发现了一个复现路径:

开启不保留活动,返回桌面,再次打开APP,能够稳定复现。

Tips: 对于一些线上问题,一些可能的复现场景:开启不保留活动、旋转屏幕、切换语言、切换暗黑模式。

日志如下:

可以看出,每次重新进入页面,parentFragment.childFragmentManager.fragments的数量每次都会增加一个。

而实际上,页面只有一个Fragment在展示数据,其他的Fragment都成了游离Fragment,没有被实际使用。

问题归因:

这个问题很有意思,因为从表现上看,这属于内存泄露了,因为存在了大量的游离/无用Fragment,导致了内存的增加。

但实际上呢,当该Activity退出时,所有的Fragment又都能销毁释放资源,又不属于内存泄露的严格定义(所有异常的内存占用都应该属于内存治理的一部分)。

这个问题的原因也很有意思,是Fragment的又一大坑,主要还是因为View/Fragment各自管理状态恢复导致的。

问题原因:

1.saveEnabled=false

此问题的发生,还是因为有开发同学在xml中,给ViewPager2设置了saveEnabled=false

xml 复制代码
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="80dp"
android:background="@color/cardview_shadow_start_color"
android:saveEnabled="false" />

追溯当时为什么加入这个属性,也是为了解决页面的泄露,在StackOverFlow上,确实有人推荐用这个参数解决页面View的泄露。

saveEnabled=false,会阻止ActivityonSaveInstanceState时,缓存页面状态,也就不会恢复页面数据,从而走重新创建流程。

2.Fragment恢复

但是这个参数,在有Fragment的时候就不灵了,因为Fragment的恢复和View的恢复是两条线。

从下面的截图可以看出,一个savedInstanceState: Bundle中,包含了四个key。

  • android:viewHierarchyState 用于恢复页面View的,比如输入框内容,滚动位置等。
  • android:fragments 用于恢复Activity下的Fragment,并且设置页面数据。

所以saveEnabled=false,确实避免了View的恢复,但是也正因为如此,后面Fragment恢复后,没有正确被复用,具体的调用链后面会贴出来。

由于Fragment_Restored仍然正常恢复了,并被添加到了FragmentStore中。

FragmentStore是真正管理Fragment的地方,supportFragmentManager.fragments,就是从FragmentStore中获取Fragment的。

但是该Fragment_Restored没有被正确添加到FragmentStateAdaptermFragments数组中,导致FragmentStateAdapter又创建了新的Fragment

循环往复,FragmentStore中的Fragment越来越多。其中只有一个是正常使用的,其他都是游离的Fragment_Restored

除了FragmentStateAdapter,还有一个FragmentPagerAdapter类里面,默认是不做状态恢复的。

二、saveEnabled的作用原理

核心逻辑分析:

saveEnabled 属性控制一个 View 及其子 View 是否参与状态保存与恢复流程(如屏幕旋转、Activity 重建)。当设置为 false 时,该 View 及其子树的状态不会被保存。

saveEnabled属性的生效机制主要体现在状态保存阶段,而非恢复阶段。

1. saveEnabled的存储机制

设置方法

kotlin 复制代码
public void setSaveEnabled(boolean enabled) {
    setFlags(enabled ? 0 : SAVE_DISABLED, SAVE_DISABLED_MASK);
}
  • enabled=true:清除SAVE_DISABLED标志位
  • enabled=false:设置SAVE_DISABLED标志位

获取方法

kotlin 复制代码
public boolean isSaveEnabled() {
    return (mViewFlags & SAVE_DISABLED_MASK) != SAVE_DISABLED;
}

2. saveEnabled的关键生效点:dispatchSaveInstanceState

核心判断逻辑

kotlin 复制代码
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
    if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
        mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
        Parcelable state = onSaveInstanceState();
        if (state != null) {
            container.put(mID, state);
        }
    }
}

关键条件分析

  1. mID != NO_IDView必须有ID
  2. (mViewFlags & SAVE_DISABLED_MASK) == 0saveEnabled必须为true

3. 状态保存的完整调用链

scss 复制代码
Activity.onSaveInstanceState()
    ↓
ViewGroup.saveHierarchyState(container)
    ↓  
ViewGroup.dispatchSaveInstanceState(container)
    ↓
遍历所有子View → View.dispatchSaveInstanceState(container)
    ↓
检查saveEnabled → View.onSaveInstanceState()

4. saveEnabled不影响状态恢复

恢复方法代码

kotlin 复制代码
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    if (mID != NO_ID) {  // 注意:这里没有检查saveEnabled!
        Parcelable state = container.get(mID);
        if (state != null) {
            onRestoreInstanceState(state);
        }
    }
}

恢复状态时只检查ID,不检查saveEnabled属性,这意味着:

  • saveEnabled只控制是否保存状态
  • 不控制是否恢复状态
  • 恢复完全依赖于是否存在已保存的状态数据

5. setSaveFromParentEnabled的独立机制

Fragment中常见的调用:

kotlin 复制代码
mFragment.mView.setSaveFromParentEnabled(false);

作用机制

kotlin 复制代码
public void setSaveFromParentEnabled(boolean enabled) {
    setFlags(enabled ? 0 : PARENT_SAVE_DISABLED, PARENT_SAVE_DISABLED_MASK);
}

这是一个独立的控制机制 ,用于控制整个View层次结构是否参与父级的状态保存遍历。

三、onRestoreInstanceState恢复的流程

这里的分析基于:

  • compileSdk 33
  • androidx.appcompat:appcompat:1.6.1

ComponentActivity.java源码:

kotlin 复制代码
override fun onCreate(savedInstanceState: Bundle?) {
    // Restore the Saved State first so that it is available to
    // OnContextAvailableListener instances
    // 恢复View状态
    savedStateRegistryController.performRestore(savedInstanceState)
    // 恢复Fragment、ViewModel等内容
    contextAwareHelper.dispatchOnContextAvailable(this)
    super.onCreate(savedInstanceState)
    ReportFragment.injectIfNeededIn(this)
    if (contentLayoutId != 0) {
        setContentView(contentLayoutId)
    }
}

1.Fragment的恢复流程

总的调用栈,后面会拆解分析

1.ComponentActivity--->onCreate

2.FragmentActivity--->restoreSaveState

3.FragmentController-->restoreSaveState

4.FragmentManager解析state,传给FragmentStore

FragmentStore是专门存放Fragment的地方

5.FragmentStore恢复Fragment,并存储

2.View恢复流程

由于View.java属于android源码,不在androidx下面,不方便debug,这里直接断点到ViewPager2的恢复流程中,查看逻辑。

saveEnabled=true 1.先调用onRestoreInstanceStatemPendingCurrentItem正确赋值 2.再调用restorePendingState,调用((StatefulAdapter) adapter).restoreState(mPendingAdapterState); 3.FragmentStateAdapter将恢复后的Fragment加入到mFragments 4.ensureFragment能够正确获取到恢复后的Fragment
saveEnabled=false 1.未调用onRestoreInstanceState,导致后续没有将恢复后的Fragment加入到mFragments 2.ensureFragment无法正确获取到恢复后的Fragment3.导致重新走了createFragment创建了新的Fragment,即使此时FragmentStore已经有可用的Fragment

3.小结

查看下方流程图,可以看到,FragmentStore虽然恢复了Fragment,但是因为saveEnabled导致部分流程没走,后续又创建了新的Fragment,那么系统恢复的Fragment就游离了,无人使用。

这里说一下我对Activity恢复流程的看法,存在两个方面的问题:

  1. 既然Activity的恢复Bundle,已经使用了不同的字段来存储Fragment/View的状态,那他们的恢复流程就应该独立互不干扰。在ViewPager2中,即使View不恢复,没有走onRestoreInstanceStateFragment恢复后,也应当能添加到FragmentStateAdaptermFragments中,完成它复用的使命。
  2. 系统提供了saveEnabled用来阻止View的恢复,却没有提供对等功能来阻止Fragment的复用。

四、Fragment保存状态到SavedInstanceState的流程:

这里拓展一下Fragment状态的保存流程,可略过

Activity会递归所有的Fragment,以及Fragment下面的Fragment,进行状态的保存

1. 触发起点:ComponentActivity.onSaveInstanceState

kotlin 复制代码
// ComponentActivity.java
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
    super.onSaveInstanceState(outState);
    mSavedStateRegistryController.performSave(outState); // 关键调用
}

作用Activity生命周期回调,系统要求保存实例状态时触发

2. 状态注册控制器:SavedStateRegistryController.performSave

kotlin 复制代码
// SavedStateRegistryController.java
public void performSave(@NonNull Bundle outBundle) {
    mRegistry.performSave(outBundle); // 委托给SavedStateRegistry
}

作用:控制器层,负责协调状态保存操作

3. 状态注册表核心:SavedStateRegistry.performSave

kotlin 复制代码
// SavedStateRegistry.java
public void performSave(@NonNull Bundle outBundle) {
    // 遍历所有已注册的状态提供者
    for (Iterator<Map.Entry<String, SavedStateProvider>> it = 
         mComponents.iteratorWithAdditions(); it.hasNext(); ) {
        Map.Entry<String, SavedStateProvider> entry = it.next();
        Bundle savedState = entry.getValue().saveState(); // 调用各个Provider
        if (savedState != null) {
            outBundle.putBundle(entry.getKey(), savedState);
        }
    }
}

作用 :遍历所有注册的Provider,执行状态保存

4. FragmentManager的Lambda回调:saveState

kotlin 复制代码
// FragmentManager中的Lambda表达式被调用
SavedStateProvider provider = () -> {
    return saveAllStateInternal(); // 执行Fragment状态保存
};

作用 :执行FragmentManager注册的状态保存逻辑

5. Fragment管理器核心保存:saveAllStateInternal

kotlin 复制代码
// FragmentManager.java
Bundle saveAllStateInternal() {
    Bundle bundle = new Bundle();
    // 保存活跃的Fragment
    mFragmentStore.saveActiveFragments(bundle);
    // 保存其他状态信息
    return bundle;
}

作用 :保存FragmentManager管理的所有Fragment状态

6. Fragment存储管理:FragmentStore.saveActiveFragments

kotlin 复制代码
// FragmentStore.java
void saveActiveFragments(@NonNull Bundle bundle) {
    // 遍历所有活跃的Fragment
    for (FragmentStateManager fragmentStateManager : mActive.values()) {
        if (fragmentStateManager != null) {
            Fragment f = fragmentStateManager.getFragment();
            Bundle result = fragmentStateManager.saveState(); // 保存单个Fragment状态
            if (result != null && !result.isEmpty()) {
                bundle.putBundle(f.mWho, result);
            }
        }
    }
}

作用 :遍历保存所有活跃Fragment的状态

7. Fragment状态管理器:FragmentStateManager.saveState

kotlin 复制代码
// FragmentStateManager.java
@NonNull Bundle saveState() {
    Bundle result = new Bundle();
    saveBasicState(result); // 保存基础状态
    return result;
}

作用 :管理单个Fragment的状态保存

8. 基础状态保存:FragmentStateManager.saveBasicState

kotlin 复制代码
// FragmentStateManager.java
void saveBasicState(@NonNull Bundle result) {
    mFragment.performSaveInstanceState(result); // 调用Fragment的保存方法
    // 保存其他基础信息
}

作用 :保存Fragment的基础状态信息

9. Fragment实例状态保存:Fragment.performSaveInstanceState

kotlin 复制代码
// Fragment.java
void performSaveInstanceState(@NonNull Bundle outState) {
    onSaveInstanceState(outState); // 调用用户重写的方法
    // 保存ChildFragmentManager状态
    mChildFragmentManager.saveAllStateInternal();
}

作用 :执行Fragment的实际状态保存逻辑

10.完整流程图

11.关键技术点

1. 注册时机

kotlin 复制代码
// FragmentManager.attachController() 中
registry.registerSavedStateProvider(SAVED_STATE_TAG, () -> {
    return saveAllStateInternal();
});

2. 状态恢复

kotlin 复制代码
// 对应的状态恢复
Bundle savedState = registry.consumeRestoredStateForKey(SAVED_STATE_TAG);
if (savedState != null) {
    restoreFromSavedState(savedState);
}

Activity开始,通过SavedState框架,最终调用到每个Fragment的状态保存方法,确保整个应用的状态能够完整保存和恢复。

五、解决方法:

方案一:直面问题,解决泄露

saveEnabled改回true,在有ViewPager2的场景,不应该使用此参数。

缺点:

需要解决页面上的View泄露,以及View泄露导致的Fragment泄露。尤其是首页这样的多团队共建页面,View的泄露不好治理

方案二:复用恢复后的Fragment

既然系统已经恢复了Fragment_Restore,我们在系统createFragment时,直接返回这个恢复Fragment_Restore,就不会再创建新的了。

结论:不行。

因为Fragment_Restore已经走完了该有的Add生命周期,createFragment后续会走add流程,而Fragment_Restore已经被add了,会报错。

ViewPagerAdapter做如下改造:

kotlin 复制代码
override fun createFragment(position: Int): Fragment {
    // 优先使用恢复后的Fragment
    val result = activity.supportFragmentManager.findFragmentByTag("f${position}")
    ?: PageFragment.newInstance(pageTitles[position]).apply {
        text = pageTitles[position]
    }

    val allFragments = activity.supportFragmentManager.fragments
    Log.i("FrankTest", "ViewPagerAdapter# createFragment size:${allFragments.size} allFragments:${allFragments.map { it.hashCode() }}")
    return result
}

运行后的崩溃:

方案三:同时清除Fragment状态

保持saveEnabled=false,但是同时清除Fragment的恢复参数

kotlin 复制代码
class LeakFragmentActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        savedInstanceState?.remove("android:support:fragments")

        setContentView(R.layout.activity_fragment_leak)
        val adapter = ViewPagerAdapter(this)
        findViewById<ViewPager2>(R.id.viewPager).adapter = adapter
        adapter.refreshData(listOf("111", "222"))
    }

}

经验证,上述代码可以解决游离Fragment的问题,多次重复步骤,supportFragmentManager.fragmentsFragment数量一直是1个。

系统的这个常量是包内访问的,应用无法直接获取:

我们可以在项目内写同包名的路径,然后指向这个包内访问的常量,在项目内就可以访问了:

优点:

快速解决问题,View不恢复,Fragment也不应该恢复。

缺点:

对于真正需要Fragment恢复的业务,此方案不适用。

建议短期先用方案三临时解决问题,再将各页面泄露情况通知到各业务团队,完全解决后,再恢复页面恢复逻辑。

相关推荐
叽哥7 小时前
Kotlin学习第 3 课:Kotlin 流程控制:掌握逻辑分支与循环的艺术
android·java·kotlin
CYRUS_STUDIO7 小时前
别让 so 裸奔!移植 OLLVM 到 NDK 并集成到 Android Studio
android·android studio·llvm
尚久龙7 小时前
安卓学习 之 图片控件和图片按钮
android·java·学习·手机·android studio·安卓
东风西巷7 小时前
Don‘t Sleep:保持电脑唤醒,确保任务不间断
android·电脑·软件需求
tangweiguo030519878 小时前
FlutterActivity vs FlutterFragmentActivity:全面对比与最佳实践
android·flutter
葱段8 小时前
【Flutter】TextField 监听长按菜单粘贴点击事件
android·flutter·dart
用户098 小时前
Gradle 现代化任务依赖方案
android·kotlin
东坡肘子9 小时前
从开放平台到受控生态:谷歌宣布 Android 开发者验证政策 | 肘子的 Swift 周报 #0101
android·swiftui·swift
脚踏实地,坚持不懈!9 小时前
ANDROID,Jetpack Compose, 贪吃蛇小游戏Demo
android