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恢复的业务,此方案不适用。

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

相关推荐
百锦再11 小时前
第11章 泛型、trait与生命周期
android·网络·人工智能·python·golang·rust·go
会跑的兔子12 小时前
Android 16 Kotlin协程 第二部分
android·windows·kotlin
键来大师12 小时前
Android15 RK3588 修改默认不锁屏不休眠
android·java·framework·rk3588
江上清风山间明月15 小时前
Android 系统超级实用的分析调试命令
android·内存·调试·dumpsys
百锦再15 小时前
第12章 测试编写
android·java·开发语言·python·rust·go·erlang
用户693717500138419 小时前
Kotlin 协程基础入门系列:从概念到实战
android·后端·kotlin
SHEN_ZIYUAN19 小时前
Android 主线程性能优化实战:从 90% 降至 13%
android·cpu优化
曹绍华19 小时前
android 线程loop
android·java·开发语言
雨白20 小时前
Hilt 入门指南:从 DI 原理到核心用法
android·android jetpack
介一安全20 小时前
【Frida Android】实战篇3:基于 OkHttp 库的 Hook 抓包
android·okhttp·网络安全·frida