4、MAT实战:分析Activity泄漏

好,咱们直接进入正题。

Activity泄漏,说白了就是Activity已经退出了,但它的对象还被别人拽着不放。GC回收不了,内存就一直在涨。我见过最夸张的一次,用户来回切换了20次页面,内存直接飙到800MB,然后App就崩了。

今天我就带你用MAT,亲手抓一个泄漏的Activity。咱们一步步来,从dump堆到分析引用链,最后把代码修好。

4.1 先制造一个泄漏场景

没有泄漏,咱们分析啥?先写一段经典的泄漏代码。你想想看,一个静态变量持有了Activity的引用,这Activity就永远别想被回收了。

public class LeakActivity extends AppCompatActivity {

    private static List<Activity> sLeakyList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak);

        // 泄漏点:静态集合持有当前Activity
        sLeakyList.add(this);

        // 模拟一个耗时任务
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(60000); // 睡60秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 注意:这里没有移除静态引用!
        // sLeakyList.remove(this);
    }
}

这段代码跑起来,你打开LeakActivity,然后按返回键退出。按理说Activity应该被销毁,但静态集合sLeakyList还拽着它。GC一看,嗯,还有人引用着,不回收。

⚠️ 注意: 静态变量持有Activity引用,是Android内存泄漏的头号杀手。我在项目中遇到过不止10次,每次都是同一个套路——忘记在onDestroy里清理。

4.2 抓取Heap Dump

代码跑起来了,怎么抓堆?我习惯用Android Studio自带的Profiler,简单直接。

  1. 打开Android Studio,点击底部的「Profiler」标签
  2. 选择你的设备和进程
  3. 点击「Memory」区域
  4. 操作App:进入LeakActivity,再按返回退出
  5. 手动触发一次GC(点击垃圾桶图标)
  6. 点击「Dump Java heap」按钮

等几秒钟,一个.hprof文件就生成好了。Android Studio会自动用MAT打开它。如果你习惯用独立的MAT工具,也可以把文件导出来,用MAT的File -> Open Heap Dump打开。

💡 小技巧: 抓dump之前,先手动GC两次。我第一次做的时候没GC,结果dump里全是正常对象,根本看不出泄漏。GC之后,泄漏的对象就「浮出水面」了。

4.3 用MAT定位泄漏的Activity

好,dump打开了。现在怎么找泄漏的Activity?

我个人习惯用「Histogram」视图。它会把所有对象按类名分组,显示每个类的实例数量和占用大小。

  1. 点击工具栏的「Histogram」按钮
  2. 在顶部的搜索框输入「LeakActivity」
  3. 你会看到LeakActivity的实例数量

正常情况下,退出后的Activity实例数应该是0。如果看到还有1个甚至多个,那就有问题了。

我遇到过最离谱的一次,Histogram里显示有47个Activity实例。用户说「我就打开了3次啊」,结果一看,每次打开都new了一个匿名内部类,那个内部类又持有了外部Activity的引用。嗯,代码写得「太灵活」了。

4.4 分析引用链:谁拽着Activity不放?

找到泄漏的Activity了,接下来要搞清楚「谁拽着它」。MAT提供了「Merge Shortest Paths to GC Roots」功能,专门干这个。

  1. 在Histogram里右键点击LeakActivity
  2. 选择「Merge Shortest Paths to GC Roots」
  3. 在弹出的菜单里选「exclude all phantom/weak/soft etc. references」

为什么要排除弱引用?因为弱引用本来就不阻止GC回收,它们不是泄漏的元凶。我们要找的是强引用链。

结果会显示一条引用路径,比如:

LeakActivity
  └─ sLeakyList (static field)
       └─ ArrayList
            └─ <Java Local>

看到了吗?sLeakyList这个静态变量,直接持有了Activity的引用。这就是泄漏的根因。

🔍 关键点: 引用链的根部通常是「GC Root」。常见的GC Root有:静态变量、活跃线程、JNI全局引用等。泄漏的Activity,它的引用链根部一定是这些之一。

4.5 修复泄漏代码

找到原因了,修起来就简单了。核心思路就一句话:在Activity销毁时,清理掉所有外部引用

修复后的代码:

public class LeakActivity extends AppCompatActivity {

    private static List<Activity> sLeakyList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak);

        sLeakyList.add(this);

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(60000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 修复:移除静态引用
        sLeakyList.remove(this);
    }
}

就加了一行代码。但你别小看这一行,它救了多少App的命啊。

不过,光加remove还不够保险。你想想看,如果onDestroy没被调用呢?比如进程被系统强杀。所以更稳妥的做法是:

  • 用WeakReference代替强引用
  • 或者在Application的onTrimMemory里做兜底清理
  • 再或者,干脆别用静态集合存Activity
💡 避坑指南: 我曾经在一个项目里,看到同事用静态HashMap存了所有Activity的引用,说是「方便统一管理」。结果每个Activity退出时都忘了remove。那一次,我花了整整一个下午,用MAT一个一个排查,最后改了30多个文件。从那以后,我定了个规矩:任何静态集合,必须配套清理逻辑,否则不许合入主干

4.6 验证修复效果

代码改完了,怎么确认修好了?再抓一次dump看看。

  1. 重新编译运行App
  2. 进入LeakActivity,再退出
  3. 手动GC,抓dump
  4. 用MAT的Histogram搜索LeakActivity

如果实例数量是0,恭喜你,修好了。如果还有,说明还有别的引用链没清理。继续用「Merge Shortest Paths to GC Roots」分析,直到彻底清除。

我一般会反复验证3次。第一次修完,第二次确认,第三次再确认。内存泄漏这东西,有时候修了一个,又冒出来一个。多验证几次,心里踏实。

4.7 总结一下

Activity泄漏的分析流程,其实就四步:

  1. 制造泄漏:写一段有问题的代码
  2. 抓dump:用Profiler或MAT抓堆快照
  3. 定位对象:用Histogram找泄漏的Activity
  4. 分析引用链:用Merge Shortest Paths找到GC Root
  5. 修复代码:在onDestroy里清理引用

这套流程,我用了好几年。不管是简单的静态变量泄漏,还是复杂的匿名内部类、Handler、Fragment嵌套泄漏,万变不离其宗。你只要掌握了MAT这个工具,再复杂的泄漏也能揪出来。

下一章,咱们聊聊更隐蔽的泄漏——匿名内部类和Handler。那才是真正考验功力的时候。