记一次关于Fragment的内存泄漏

本文分析了单Activity多Fragment架构中,LoginFragment关闭后由于FragmentManangerImpl中的mCreatedMenus引用导致的内存泄漏问题。通过使用leakcanary和AndroidProfiler工具定位问题,并提出了两种解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

之前使用单Activity多Fragment架构完成过一个项目,在后期维护时发现,很多Fragment在关闭之后,内存无法被回收,出现了内存泄漏问题。leakcanary显示引用链信息如下:
ganduwei
LoginFragment关闭后仍然被FragmentManangerImpl中的mCreatedMenus所引用,导致LoginFragment无法被释放。

我又使用Android Profiler工具查看了内存中的实例,步骤如下:
1、运行app,打开Android Profiler工具,然后点击MEMORY,会显示如下视图:
ganduwei
2、点击左上角的“Force garbage collection”按钮执行一次垃圾回收,再点击它旁边的“Dump Java heap”按钮便可将堆中的实例全部分析出来:
ganduwei
如上图我把fragment过滤出来,LoginFragment确实还在内存中,但是FragmentManangerImpl中mAdded中已经不存在LoginFragment,再看看mCreatedMenus:
ganduwei
里面包含了LoginFragment。mCreatedMenus是个什么东西呢,为什么会引用着已关闭的页面?它是FragmentManangerImpl中的属性,只能去FragmentManangerImpl中找找答案了。

在FragmentManangerImpl找到了如下一段代码:

    public boolean dispatchCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        if (this.mCurState < 1) {
            return false;
        } else {
            boolean show = false;
            ArrayList<Fragment> newMenus = null;

            int i;
            Fragment f;
            for(i = 0; i < this.mAdded.size(); ++i) {
                f = (Fragment)this.mAdded.get(i);
                if (f != null && f.performCreateOptionsMenu(menu, inflater)) {
                    show = true;
                    if (newMenus == null) {
                        newMenus = new ArrayList();
                    }

                    newMenus.add(f);
                }
            }

            if (this.mCreatedMenus != null) {
                for(i = 0; i < this.mCreatedMenus.size(); ++i) {
                    f = (Fragment)this.mCreatedMenus.get(i);
                    if (newMenus == null || !newMenus.contains(f)) {
                        f.onDestroyOptionsMenu();
                    }
                }
            }

            this.mCreatedMenus = newMenus;
            return show;
        }
    }

mCreatedMenus会在dispatchCreateOptionsMenu方法中被初始化并赋值,而其他地方并没有改变或重置mCreatedMenus中的元素,所以只有dispatchCreateOptionsMenu方法会影响mCreatedMenus。
dispatchCreateOptionsMenu方法是干嘛的呢,其实从方法名就可以猜到它是用来分发创建ActionBar菜单的。的确,从代码中我们就可以看出,它遍历了mAdded中所有的元素,然后调用元素的performCreateOptionsMenu方法。既然与创建菜单有关,那就去LoginFragment看看。

LoginFragment有创建Toolbar,并把Toolbar交给Activity来创建菜单,代码如下:

	setHasOptionsMenu(true);
	((AppCompatActivity) getActivity()).setSupportActionBar(mToolbar);

而LoginFragment上一级Fragment中并没有创建Toolbar菜单。

到这里我们大致就知道原因了,有两个地方:

1、LoginFragment中有创建菜单,而它的上一级Fragment没有创建菜单,这样导致从LoginFragment返回到上一级后,AppCompatActivity中的FragmentManangerImpl没有执行dispatchCreateOptionsMenu方法,所有mCreatedMenus中还是保存了LoginFragment的实例。如果上一级Fragment有创建菜单不会有此问题;

2、((AppCompatActivity) getActivity()).setSupportActionBar(mToolbar)这句代码导致Activity中引用了Fragment的mToolbar,如果Fragment关闭后,没有去掉这个引用就会导致无法释放Fragment。

当然,如果一个Activity中只有一两个Fragment没什么关系,Activity销毁后也就随之销毁了。但是在单Activity项目中,整个App就一个Activity,被关闭的Fragment在整个App生命周期中一直存在,这样就有很大问题了。

找到原因后,分析出解决方法就比较简单了。解决办法有两个:
1、Fragment中的菜单由自己来创建,不交给Activity,代码如下:

        mToolbar.setNavigationIcon(R.drawable.ic_back);
        mToolbar.setNavigationOnClickListener(v -> {
            //TODO
        });
        mToolbar.inflateMenu(R.menu.toolbar_menu);
        mToolbar.setOnMenuItemClickListener(menuItem -> {
            //TODO
            return true;
        });

2、菜单还是交给Activity管理,如果上一级Fragment有创建菜单那不用处理,如果没有需要在上一级Fragment清除掉引用,代码如下:

        ((AppCompatActivity) getActivity()).setSupportActionBar(null);
        ( getActivity()).onCreatePanelMenu(0,null);

onCreatePanelMenu方法会使dispatchCreateOptionsMenu被调用,从而给mCreatedMenus重新赋值。

当然最好是使用第一个方法,每个Fragment中的菜单由自己来管理。

<think>好的,我需要帮助用户设计一个逻辑来检测APK在循环运行50次操作后是否存在内存泄漏。首先,我得理清楚内存泄漏检测的基本原理和常用方法。 用户提到的是APK测试,所以可能需要用到Android开发中的工具,比如Android Profiler或者LeakCanary这样的库。不过用户可能希望自己编写一个自动化测试的逻辑,而不是依赖现有工具。我需要考虑如何在没有这些工具的情况下,通过代码逻辑来判断内存泄漏。 首先,内存泄漏通常是因为某些对象在不再需要时没有被垃圾回收(GC)释放,导致内存使用量逐渐增加。因此,检测内存泄漏的一个方法是监控内存的使用情况,特别是在多次操作后是否持续增长。 接下来,用户希望循环跑50次操作。这里的“操作”可能是指某个特定的功能或流程,比如打开关闭一个Activity,执行某个任务等。每次操作后,需要检查是否有对象未被释放。 那么逻辑的大致步骤可能是这样的: 1. **初始化环境**:启动应用,获取初始内存状态。 2. **执行单次操作**:模拟用户的操作,比如启动一个Activity,完成某些任务后关闭它。 3. **触发垃圾回收**:在每次操作结束后,手动触发GC(虽然不能保证立即回收,但可以促进)。 4. **录内存使用**:每次操作后录内存使用量,或者跟踪特定对象的实例数量。 5. **循环执行**:重复步骤2-4共50次。 6. **分析数据**:检查内存是否持续增长,或者是否有对象实例未被释放。 不过这里有几个问题需要考虑: - **手动触发GC的可靠性**:Android中System.gc()并不保证立即执行,所以可能需要多次触发或者等待一段时间。 - **内存波动因素**:除了泄漏,内存使用可能因为其他因素波动,需要多次采样取平均值。 - **检测对象泄漏**:更直接的方式是跟踪特定对象的实例,比如Activity或Fragment,确保在销毁后实例被回收。 可能的优化点: - 使用弱引用(WeakReference)来检测对象是否被回收。例如,在Activity销毁后,创建一个弱引用,触发GC后检查该引用是否已被清除。 - 使用Android的ActivityLifecycleCallbacks来监听Activity的创建和销毁,统计未被销毁的实例数量。 - 结合dumpsys meminfo命令来获取更详细的内存信息,分析Java堆内存的变化。 具体逻辑步骤可能如下: 1. **设置测试循环**:循环50次执行目标操作。 2. **每次循环前录初始内存状态**:比如当前堆内存使用量,或特定对象的实例数。 3. **执行操作**:例如启动并关闭一个Activity。 4. **强制GC并等待**:调用System.gc()并等待一段时间,确保回收完成。 5. **检查内存或对象实例**:确认Activity实例是否被回收,或者内存是否回落。 6. **录每次结果**:如果发现内存持续增长或对象未被释放,则录泄漏。 7. **汇总分析**:50次循环后,统计泄漏发生的次数,判断是否存在内存泄漏。 另外,需要考虑测试的稳定性,比如避免其他应用干扰,确保测试环境一致。可能需要在真机或模拟器上多次运行,排除偶然因素。 最后,可能需要编写自动化测试脚本,结合AndroidJUnitRunner或者Espresso来执行循环操作,并通过adb命令获取内存信息。或者直接在应用内集成检测代码,输出日志进行分析。</think>检测APK内存泄漏的循环测试逻辑可按以下步骤设计: ### 一、基础检测逻辑(适合简单场景) 1. **初始化基准内存** ```java // 首次启动后获取初始内存值 long baseMemory = getCurrentMemoryUsage(); ``` 2. **循环测试模块** ```java for(int i=0; i<50; i++){ // 执行目标操作(如启动/关闭Activity) performTargetOperation(); // 强制触发垃圾回收 System.gc(); System.runFinalization(); // 录内存数据 logMemoryUsage(i); // 检测关键对象(如Activity实例) if(checkActivityLeaked()){ markAsLeaked(); } } ``` 3. **内存分析算法** ```python # 检测持续增长趋势 if current_memory > base_memory + 10MB * (cycle_count/5): return "Memory Leak Detected" ``` ### 二、增强检测方案(推荐方案) #### 1. 对象追踪检测 ```kotlin val weakRefMap = WeakHashMap<Any, String>() fun performOperation() { val targetObject = createPotentialLeakObject() weakRefMap[targetObject] = "MARKER" // 操作完成后应销毁对象... } fun checkLeak(): Boolean { System.gc() return weakRefMap.size() > threshold // 残存对象超阈值则泄漏 } ``` #### 2. 生命周期监控(Activity泄漏检测) ```java class LeakDetector implements Application.ActivityLifecycleCallbacks { private Set<Activity> aliveActivities = new HashSet<>(); @Override public void onActivityDestroyed(Activity activity) { if(!aliveActivities.remove(activity)){ log("异常情况:未录的Activity被销毁"); } } public boolean hasLeak() { return !aliveActivities.isEmpty(); } } ``` ### 三、自动化测试架构 1. **测试框架选择** - 使用Espresso/UIAutomator控制界面操作 - 集成LeakCanary进行自动化分析 - 通过ADB监控内存: ```bash adb shell dumpsys meminfo <package_name> ``` 2. **执行流程设计** ``` 开始 ├─ 冷启动应用 ├─ 录初始内存状态 ├─ 进入测试循环(50次): │ ├─ 执行预设操作流 │ ├─ 返回初始界面 │ ├─ 强制GC(3次确保回收) │ ├─ 抓取内存快照 │ └─ 检查悬挂对象 ├─ 对比内存增长曲线 ├─ 分析残留对象 └─ 生成检测报告 ``` ### 四、关键判断指标 1. **内存增长判断标准** - Java堆内存单次增长 > 200KB - Native内存持续增长 - 总内存50次累计增长 > 15MB 2. **对象泄漏判断** - Activity实例残留 > 3个 - Fragment残留 > 5个 - Bitmap未回收量持续增加 ### 五、注意事项 1. **环境控制** - 固定测试设备型号/系统版本 - 关闭后台无关进程 - 禁用系统自动更新 2. **结果验证** ```python # 排除正常内存波动 if 连续5次增长且Pearson相关系数 > 0.7: 判定为真实泄漏 ``` 3. **优化建议** - 结合HPROF文件分析 - 使用Android Studio Profiler实时监控 - 在低端设备上更容易暴露问题 建议采用组合检测方案,同时监控内存走势和对象实例数量,每次循环后增加2秒等待时间确保系统完成资源回收。测试通过标准应设置为:50次循环后内存波动范围不超过±5%,且无关键对象残留。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值