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一看,嗯,还有人引用着,不回收。
4.2 抓取Heap Dump
代码跑起来了,怎么抓堆?我习惯用Android Studio自带的Profiler,简单直接。
- 打开Android Studio,点击底部的「Profiler」标签
- 选择你的设备和进程
- 点击「Memory」区域
- 操作App:进入LeakActivity,再按返回退出
- 手动触发一次GC(点击垃圾桶图标)
- 点击「Dump Java heap」按钮
等几秒钟,一个.hprof文件就生成好了。Android Studio会自动用MAT打开它。如果你习惯用独立的MAT工具,也可以把文件导出来,用MAT的File -> Open Heap Dump打开。
4.3 用MAT定位泄漏的Activity
好,dump打开了。现在怎么找泄漏的Activity?
我个人习惯用「Histogram」视图。它会把所有对象按类名分组,显示每个类的实例数量和占用大小。
- 点击工具栏的「Histogram」按钮
- 在顶部的搜索框输入「LeakActivity」
- 你会看到LeakActivity的实例数量
正常情况下,退出后的Activity实例数应该是0。如果看到还有1个甚至多个,那就有问题了。
我遇到过最离谱的一次,Histogram里显示有47个Activity实例。用户说「我就打开了3次啊」,结果一看,每次打开都new了一个匿名内部类,那个内部类又持有了外部Activity的引用。嗯,代码写得「太灵活」了。
4.4 分析引用链:谁拽着Activity不放?
找到泄漏的Activity了,接下来要搞清楚「谁拽着它」。MAT提供了「Merge Shortest Paths to GC Roots」功能,专门干这个。
- 在Histogram里右键点击LeakActivity
- 选择「Merge Shortest Paths to GC Roots」
- 在弹出的菜单里选「exclude all phantom/weak/soft etc. references」
为什么要排除弱引用?因为弱引用本来就不阻止GC回收,它们不是泄漏的元凶。我们要找的是强引用链。
结果会显示一条引用路径,比如:
LeakActivity
└─ sLeakyList (static field)
└─ ArrayList
└─ <Java Local>
看到了吗?sLeakyList这个静态变量,直接持有了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
4.6 验证修复效果
代码改完了,怎么确认修好了?再抓一次dump看看。
- 重新编译运行App
- 进入LeakActivity,再退出
- 手动GC,抓dump
- 用MAT的Histogram搜索LeakActivity
如果实例数量是0,恭喜你,修好了。如果还有,说明还有别的引用链没清理。继续用「Merge Shortest Paths to GC Roots」分析,直到彻底清除。
我一般会反复验证3次。第一次修完,第二次确认,第三次再确认。内存泄漏这东西,有时候修了一个,又冒出来一个。多验证几次,心里踏实。
4.7 总结一下
Activity泄漏的分析流程,其实就四步:
- 制造泄漏:写一段有问题的代码
- 抓dump:用Profiler或MAT抓堆快照
- 定位对象:用Histogram找泄漏的Activity
- 分析引用链:用Merge Shortest Paths找到GC Root
- 修复代码:在onDestroy里清理引用
这套流程,我用了好几年。不管是简单的静态变量泄漏,还是复杂的匿名内部类、Handler、Fragment嵌套泄漏,万变不离其宗。你只要掌握了MAT这个工具,再复杂的泄漏也能揪出来。
下一章,咱们聊聊更隐蔽的泄漏——匿名内部类和Handler。那才是真正考验功力的时候。