Android 深入剖析Android内存泄漏:ViewPager2与Fragment的生命周期陷阱

场景: 在新能源车机系统有一套UI页面,手机有竖屏页面,手机又横屏页面,3种情况相互切换,频繁的切换,手机的竖屏页面有3个tab和3个对应fragment,一个外层Fragment包含3个fragment, 不停的切换导致的!

1.内存泄漏信息

yaml 复制代码
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │ GC Root: Global variable in native code
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │    Leaking: NO (MainActivity↓ is not leaking)
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │    ↓ -$$Lambda$ViewRootImpl$zmAX2p20-kqxknxcUyGhSNjsJvM.f$0
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │    Leaking: NO (MainActivity↓ is not leaking)
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │    mContext instance of com.tencent.settings.MainActivity with mDestroyed = false
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │    ViewRootImpl#mView is not null
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │    ↓ ViewRootImpl.mContext
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  ├─ com.tencent.settings.MainActivity instance
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │    ↓ ComponentActivity.mLifecycleRegistry
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │                        ~~~~~~~~~~~~
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │    Retaining 52 B in 2 objects
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │    Anonymous class implementing androidx.lifecycle.LifecycleEventObserver
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  ├─ androidx.viewpager2.adapter.FragmentStateAdapter$FragmentMaxLifecycleEnforcer instance
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │    ↓ FragmentStateAdapter$FragmentMaxLifecycleEnforcer.mViewPager
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │                                                        ~~~~~~~~~~
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  ├─ androidx.viewpager2.widget.ViewPager2 instance
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │    Leaking: UNKNOWN
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │    Retaining 85.1 kB in 1109 objects
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │    View not part of a window view hierarchy
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │    View.mAttachInfo is null (view detached)
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │    View.mWindowAttachCount = 1
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │    mContext instance of com.tencent.settings.MainActivity with mDestroyed = false
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │    ↓ View.mParent
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  │           ~~~~~~~
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  ​     Leaking: YES (ObjectWatcher was watching this because com.tencent.settings.PhoneAndroidFragment received
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  ​     Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  ​     key = 4e6eb9f9-742a-42b5-a71d-42543f7f1407
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  ​     watchDurationMillis = 22525
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  ​     View not part of a window view hierarchy
2025-12-11 09:35:44.808 57579-61121 LeakCanary              com.tencent.settings                     D  ​     View.mWindowAttachCount = 1

泄漏现象

LeakCanary的日志显示了这样一个泄漏链:

ini 复制代码
GC Root: Global variable in native code
↓
ViewRootImpl的Lambda表达式
↓
MainActivity (mDestroyed = false)
↓
ComponentActivity.mLifecycleRegistry
↓
FragmentStateAdapter$FragmentMaxLifecycleEnforcer
↓
ViewPager2实例 (已从窗口分离)
↓
Fragment已销毁的视图 (应被回收但未被回收)

2. 主要泄漏点

PhoneAndroidFragment 已经收到了 onDestroyView() 回调

但其视图仍然被 ViewPager2 持有

ViewPager2 本身又被 FragmentMaxLifecycleEnforcer 持有

2.2 核心原因

FragmentStateAdapter$FragmentMaxLifecycleEnforcer 是一个生命周期观察者,它:

通过 MainActivity 的 mLifecycleRegistry 注册

持有 ViewPager2 的引用

导致 ViewPager2 及其子视图无法被释放

问题:谁持有 ViewPager2, 为什么PhoneAndroidFragment 不能被回收

markdown 复制代码
1. GC Root (Native全局变量)
   ↓
2. ViewRootImpl的Lambda函数
   ↓
3. MainActivity (Activity本身未销毁)
   ↓
4. ComponentActivity.mLifecycleRegistry (生命周期注册表)
   ↓
5. FragmentStateAdapter$FragmentMaxLifecycleEnforcer (生命周期观察者)
   ↓
6. ViewPager2 实例 (关键持有者)
   ↓
7. ViewPager2 的内部状态 (RecyclerView、适配器等)
   ↓
8. PhoneAndroidFragment 的视图 (Fragment的View层次结构)

3.源码详细分析各环节:

3.1. FragmentMaxLifecycleEnforcer 是谁?
scala 复制代码
// 这是ViewPager2的内部类,负责管理Fragment的生命周期
public class FragmentStateAdapter extends RecyclerView.Adapter<...> {
    class FragmentMaxLifecycleEnforcer {
        // 关键字段
        private ViewPager2 mViewPager;  // 持有ViewPager2的强引用
        private LifecycleEventObserver mLifecycleObserver;  // 生命周期观察者
    }
}
3.2. 为什么它持有ViewPager2?

当ViewPager2使用FragmentStateAdapter时,会自动创建一个FragmentMaxLifecycleEnforcer实例:

less 复制代码
// 在FragmentStateAdapter构造函数中
public FragmentStateAdapter(@NonNull FragmentManager fragmentManager, 
                           @NonNull Lifecycle lifecycle) {
    // ...
    mFragmentMaxLifecycleEnforcer = new FragmentMaxLifecycleEnforcer();
    lifecycle.addObserver(mFragmentMaxLifecycleEnforcer);  // 注册到Activity生命周期
}
3.3. PhoneAndroidFragment为什么不能被回收?

根本原因:ViewPager2仍然持有Fragment的视图引用

scss 复制代码
// ViewPager2内部是通过RecyclerView实现的
// 当Fragment被销毁时,如果ViewPager2没有正确清理,就会:
1. ViewPager2的RecyclerView仍然缓存着Fragment的ViewHolder
2. Fragment的View仍然在ViewPager2的子View列表中
3. Fragment的onDestroyView()被调用,但View对象没有被GC回收

3.4 RecyclerView缓存问题

arduino 复制代码
// ViewPager2内部使用RecyclerView,有以下缓存:
1. mAttachedScrap - 已附加的废弃视图
2. mCachedViews - 缓存视图(默认大小2)
3. mRecyclerPool - 回收池

// 当Fragment销毁时,如果View还在这些缓存中:
// 1. View不会被释放
// 2. Fragment的视图引用被保留
// 3. 导致内存泄漏

3.5. 屏幕切换影响

markdown 复制代码
车机系统 ↔ 手机竖屏 ↔ 手机横屏
      ↓         ↓         ↓
布局完全不同   包含ViewPager2  布局完全不同
      ↓         ↓         ↓
每次切换需要   3个Tab频繁   每次切换需要
完全重建UI   切换Fragment  完全重建UI

具体的引用路径:

markdown 复制代码
PhoneAndroidFragment的View被以下对象引用:
1. ViewPager2.mRecyclerView (RecyclerView实例)
   ↓
2. RecyclerView.mRecycler (RecyclerView.Recycler回收池)
   ↓
3. RecyclerView.Recycler.mCachedViews (缓存视图列表)
   ↓
4. 或 RecyclerView.Recycler.mAttachedScrap (附加的废弃视图)
   ↓
5. Fragment的View在缓存中等待复用

为什么会形成这个泄漏链?

场景重现
  1. 初始化

    ini 复制代码
    // MainActivity中
    ViewPager2 viewPager = findViewById(R.id.view_pager);
    viewPager.adapter = new FragmentStateAdapter(this);
    // 此时,FragmentMaxLifecycleEnforcer被注册到Activity的生命周期
  2. Fragment被销毁

    arduino 复制代码
    // 当PhoneAndroidFragment收到onDestroyView()时
    // 理想情况:ViewPager2应该释放对Fragment View的引用
    // 实际情况:ViewPager2可能还在缓存这个View
  3. 问题发生

    less 复制代码
    // ViewPager2的FragmentStateAdapter没有正确处理
    public void onViewRecycled(@NonNull FragmentViewHolder holder) {
        // 可能没有正确调用FragmentTransaction.remove()
        // 或者Fragment的View还在RecyclerView的缓存中
    }

整体的流程图:

4. 日志深度分析

4.1. GC Root溯源

日志显示GC Root是Global variable in native code,这表明泄漏的源头在Android系统层。具体来说,是ViewRootImpl的Lambda表达式被native层的全局变量持有。

ViewRootImpl是Android窗口系统的核心组件,负责连接应用层的View和系统层的Window。当Activity创建窗口时,系统会创建对应的ViewRootImpl,并在native层注册各种回调。

4.2. 引用链分析

让我们逐级分析这个引用链:

  1. 系统层持有:native层的全局变量持有ViewRootImpl的Lambda引用
  2. Lambda捕获:该Lambda捕获了外部类的引用(即MainActivity)
  3. Activity存活 :MainActivity的mDestroyed = false,表明Activity并未销毁
  4. 生命周期注册 :Activity的mLifecycleRegistry持有FragmentMaxLifecycleEnforcer
  5. ViewPager2关联FragmentMaxLifecycleEnforcer持有ViewPager2的引用
  6. 视图未被释放 :ViewPager2持有已调用onDestroyView()的Fragment视图

4.3. 关键状态信息

从日志中我们注意到几个关键状态:

  • View.mAttachInfo is null (view detached):ViewPager2已从窗口分离
  • View not part of a window view hierarchy:视图不在窗口层级中
  • mDestroyed = false:Activity未销毁
  • PhoneAndroidFragment received Fragment#onDestroyView():Fragment的视图已被销毁

问题本质

生命周期错配

这个问题的核心是生命周期注册在错误的层级

ini 复制代码
// 错误写法:注册到Activity生命周期
adapter = new ViewPagerAdapter(getActivity());

// 正确写法:注册到Fragment生命周期  
adapter = new ViewPagerAdapter(this);

当ViewPager2的FragmentStateAdapter接收到Activity作为参数时,它内部创建的FragmentMaxLifecycleEnforcer会注册到Activity的生命周期。这意味着:

  • 即使Fragment的视图被销毁,FragmentMaxLifecycleEnforcer仍然存活
  • FragmentMaxLifecycleEnforcer持有ViewPager2的引用
  • ViewPager2持有Fragment已销毁的视图
  • 导致Fragment视图无法被GC回收

ViewRootImpl的介入

为什么系统层的ViewRootImpl会参与这个泄漏链?

当Activity显示时,系统会创建ViewRootImpl来管理窗口。ViewRootImpl可能创建一些Lambda回调(如窗口焦点变化、输入事件处理等),这些Lambda隐式捕获了Activity引用。由于这些回调被native层全局变量引用,它们成为了GC Root,阻止了Activity及其关联对象的回收。

5. 解决方案

方案一:修正生命周期注册(核心修复)

核心要记住:FragmentStateAdapter必须使用Fragment的生命周期,而非Activity,这是解决此类问题的关键。

修改ViewPagerAdapter的构造函数调用

csharp 复制代码
// PhoneAndroidFragment.java
private void initView() {
    // 将原有的:
    // adapter = new ViewPagerAdapter(getActivity());
    // 改为:
    adapter = new ViewPagerAdapter(this);  // 传递Fragment本身
}

增强ViewPagerAdapter支持Fragment

less 复制代码
// ViewPagerAdapter.java
public class ViewPagerAdapter extends FragmentStateAdapter {
    
    // 保留原有的Activity构造函数
    public ViewPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
        super(fragmentActivity);
    }
    
    // 新增Fragment构造函数(关键修复)
    public ViewPagerAdapter(@NonNull Fragment fragment) {
        super(fragment);
    }
    
    // ... 其他代码
}

方案二:彻底清理onDestroyView()

ini 复制代码
@Override
public void onDestroyView() {
    isDestroyed = true;
    
    // 1. 清理Handler
    if (handler != null) {
        handler.removeCallbacksAndMessages(null);
    }
    
    // 2. 取消ViewPager2回调
    if (viewDataBinding != null && pageChangeCallback != null) {
        viewDataBinding.phoneSettingPager.unregisterOnPageChangeCallback(pageChangeCallback);
        pageChangeCallback = null;
    }
    
    // 3. 清理ViewPager2适配器
    if (viewDataBinding != null && viewDataBinding.phoneSettingPager != null) {
        // 清理内部RecyclerView
        View child = viewDataBinding.phoneSettingPager.getChildAt(0);
        if (child instanceof RecyclerView) {
            RecyclerView recyclerView = (RecyclerView) child;
            recyclerView.setAdapter(null);
            recyclerView.setLayoutManager(null);
            recyclerView.getRecycledViewPool().clear();
        }
        
        // 清理ViewPager2适配器
        viewDataBinding.phoneSettingPager.setAdapter(null);
    }
    
    // 4. 清理适配器引用
    adapter = null;
    
    EventBusUtils.unRegister(this);
    super.onDestroyView();
}

方案三:添加状态检查和防护

scss 复制代码
// 添加状态检查方法
private boolean isFragmentUsable() {
    return !isDestroyed && 
           isAdded() && 
           getActivity() != null && 
           !getActivity().isFinishing() && 
           getView() != null;
}

// 在所有回调中使用状态检查
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEvent(SomeEvent event) {
    if (!isFragmentUsable()) return;
   
}

方案四:使用WeakReference包装回调

scala 复制代码
// 安全的回调包装类
class SafePageChangeCallback extends ViewPager2.OnPageChangeCallback {
    private WeakReference<PhoneAndroidFragment> fragmentRef;
    
    public SafePageChangeCallback(PhoneAndroidFragment fragment) {
        this.fragmentRef = new WeakReference<>(fragment);
    }
    
    @Override
    public void onPageSelected(int position) {
        PhoneAndroidFragment fragment = fragmentRef.get();
        if (fragment == null || !fragment.isFragmentUsable()) {
            return;
        }
    
    }
}
相关推荐
我是伪码农6 小时前
Vue 1.23
前端·javascript·vue.js
2601_949575868 小时前
Flutter for OpenHarmony二手物品置换App实战 - 商品卡片实现
android·flutter
2601_9495758610 小时前
Flutter for OpenHarmony二手物品置换App实战 - 表单验证实现
android·java·flutter
毕设源码-郭学长11 小时前
【开题答辩全过程】以 基于Web的高校课程目标达成度系统设计与实现为例,包含答辩的问题和答案
前端
wuhen_n12 小时前
高阶函数与泛型函数的类型体操
前端·javascript·typescript
龚礼鹏12 小时前
图像显示框架八——BufferQueue与BLASTBufferQueue(基于android 15源码分析)
android·c语言
1登峰造极13 小时前
uniapp 运行安卓报错reportJSException >>>> exception function:createInstanceContext, exception:white screen
android·java·uni-app
木易 士心13 小时前
Android Handler 机制原理详解
android
kkk_皮蛋13 小时前
作为一个学生,如何用免费 AI 工具手搓了一款 Android AI 日记 App
android·人工智能
金山毒霸电脑医生13 小时前
植物大战僵尸杂交版最新v0.2版下载安装|2025图文解析教程
android·游戏·ios·植物大战僵尸·软件下载安装