内存泄漏在Android开发中是个绕不开的话题。应用运行时间越长,泄漏的对象积累越多,可用堆内存持续下降,最终表现为界面卡顿、GC频繁,甚至直接抛出OOM异常崩溃。更麻烦的是,泄漏的代码往往写起来毫无破绽,编译运行一切正常,问题只在长时间使用后才暴露。这篇文章先梳理几种最常见的泄漏场景,再介绍如何用MAT工具把泄漏对象和引用链挖出来。

一、静态变量持有Activity或View
这是最典型也最容易被忽视的泄漏。Activity本来应该由系统在其生命周期结束后回收,但如果一个静态变量直接或间接持有了Activity的引用,垃圾回收器就永远无法判定它为不可达对象。举个例子,有些开发者为了在工具类里弹Toast方便,直接声明了public static Context mContext并传入Activity,这个写法在当前页面看不出任何问题,但只要App进程不销毁,这个Activity就永远活在堆里。
类似的场景还包括静态View、静态Drawable、静态Bitmap。有人喜欢把某个复杂的自定义View缓存成静态变量以便下次复用,但View内部会持有Activity的Context,结果整个Activity连同它的视图树、图片资源全部泄漏。修复思路很简单:静态变量只持有ApplicationContext,或者在使用完毕后显式置空引用。ApplicationContext的生命周期与进程一致,被静态变量持有不会造成泄漏,但要警惕它不能用于创建Dialog等需要窗口主题的组件。
public class LeakDemo {
// 错误写法:静态变量持有Activity引用
private static Activity sActivity;
public static void init(Activity activity) {
sActivity = activity;
}
// 正确写法:使用ApplicationContext或及时置空
private static Context sAppContext;
public static void init(Context context) {
sAppContext = context.getApplicationContext();
}
}二、Handler、内部类与异步线程泄漏
非静态内部类会隐式持有外部类的引用,这条规则是Handler泄漏的根源。当我们在Activity里直接new一个Handler并sendMessageDelayed发送延时消息时,Message对象会持有Handler,Handler又持有Activity。如果消息的延迟时间是60秒,那么在这60秒内即使用户已经退出了Activity,这个Activity依然被消息队列牢牢拽着,无法回收。
解决方法是把它改成静态内部类加WeakReference的写法,静态内部类不持有外部引用,弱引用不会阻止GC回收Activity。同时别忘了在Activity销毁时调用removeCallbacksAndMessages(null)清空消息队列。同样的道理也适用于AsyncTask和Thread:一个匿名内部类线程如果执行了耗时任务,在任务结束前Activity都无法回收。使用线程时要么改成静态类,要么在onDestroy里中断线程。
public class SafeActivity extends Activity {
private final MyHandler mHandler = new MyHandler(this);
private static class MyHandler extends Handler {
private final WeakReference<SafeActivity> mRef;
MyHandler(SafeActivity activity) {
mRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
SafeActivity activity = mRef.get();
if (activity == null || activity.isFinishing()) {
return; // Activity已被回收,直接返回
}
// 正常处理消息
}
}
@Override
protected void onDestroy() {
mHandler.removeCallbacksAndMessages(null);
super.onDestroy();
}
}三、监听器、广播与单例回调未注销
注册型API是泄漏的高发区。典型例子包括BroadcastReceiver的registerReceiver、LocationManager的requestLocationUpdates、各类EventBus的register、SDK推送服务的回调绑定等。这些API的共同特点是:系统级或全局级对象持有了你传入的监听器,而监听器又持有Activity。如果注销的代码没有写在对应的生命周期方法里,或者注销时机与注册时机不对称,泄漏就发生了。
单例模式的回调同样是重灾区。很多网络请求库、图片加载库支持设置全局回调,如果单例持有了某个页面的回调对象,且该回调是匿名内部类形式,那么页面销毁后回调仍挂在单例上。排查这类问题时优先检查代码中所有register开头的调用,确认每一处都有成对的unregister或removeListener。建议在BaseActivity里统一管理监听器的注册与注销,避免遗漏。
public class MonitorActivity extends Activity {
private BroadcastReceiver mReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mReceiver = new BroadcastReceiver() { // 匿名内部类持有Activity
@Override
public void onReceive(Context context, Intent intent) { }
};
registerReceiver(mReceiver, new IntentFilter("com.test.ACTION"));
}
@Override
protected void onDestroy() {
unregisterReceiver(mReceiver); // 必须成对注销,否则泄漏
super.onDestroy();
}
}四、使用MAT定位泄漏对象
发现内存疑似泄漏后,需要用工具拿到证据。MAT(Memory Analyzer Tool)是Eclipse基金会出品的堆分析工具,功能比Android Studio自带的Profiler更深入。第一步是导出hprof文件:在Android Studio的Profiler中点击Dump Java Heap,然后在左侧面板点击导出按钮获得hprof;也可以在命令行用adb shell am dumpheap com.example.app /data/local/tmp/dump.hprof导出。注意Android平台导出的文件需要先用platform-tools目录下的hprof-conv工具转换格式:hprof-conv dump.hprof converted.hprof,否则MAT无法解析。
用MAT打开转换后的文件,首先要看的是Overview页的Leak Suspects报告,它会自动猜测最可疑的泄漏点并画出饼图。但自动报告只是参考,真正的排查要靠三个视图:Histogram按类维度统计对象数量和浅堆大小;Dominator Tree按对象维度列出支配树,能看出哪个对象拖住了最大的保留堆;Path to GC Roots则回答最关键的问题——这个对象为什么没被回收。
具体操作是:打开Histogram,在正则过滤框输入Activity的类名,观察已退出页面的实例数量是否大于0。如果某个应该销毁的Activity还残留着,右键该类选择Merge Shortest Paths to GC Roots,再勾选exclude weak/soft references排除弱引用,剩下的引用链就是真正的泄漏路径。链上通常能直接看到静态变量、消息队列或者某个单例的字段,对应回源码就能定位问题。
// MAT操作流程 1. Android Studio Profiler -> Dump Java Heap -> 导出hprof 2. hprof-conv input.hprof output.hprof 3. MAT打开output.hprof 4. 打开Histogram,过滤 Activity 类名 5. 右键 -> Merge Shortest Paths to GC Roots -> exclude all phantom/weak/soft etc. references 6. 分析引用链,定位泄漏源头
五、实战建议与预防措施
排查内存泄漏时有个实用技巧:反复进入退出可疑页面十次左右,然后dump堆内存。如果该页面的Activity实例数明显大于1,基本可以确认存在泄漏;实例数减一就是泄漏的次数。另外MAT的Compare Basket功能可以对比操作前后的两份堆快照,差异部分往往就是泄漏的对象,这比单看一份快照高效得多。
从预防角度看,建议把LeakCanary集成到Debug构建中,它会在页面销毁后主动检测泄漏并给出引用链,能在开发阶段拦截大部分问题。编码时遵守几条原则:静态成员只持有ApplicationContext、生命周期短的对象不要注册到生命周期长的对象上、所有注册型API在onDestroy中成对注销、耗时任务使用静态内部类加弱引用。结合LeakCanary的日常预警和MAT的深度分析,内存泄漏问题就能做到早发现、快定位、彻底修复。
Android内存泄漏MAT分析Memory Analyzer Tool修改时间:2026-09-05 08:34:37