3、内存泄漏检测与修复:LeakCanary原理与使用、常见泄漏场景、MAT与Android Studio Profiler实战
内存泄漏,说白了就是本该被回收的对象,因为被某个不该存在的引用拽着,死活走不了。GC看着它干瞪眼。时间一长,堆内存越占越多,最后就是OOM,应用直接崩给你看。
我刚开始做Android那会儿,对内存泄漏其实没太当回事。直到有一次,一个社交App的首页,用户滑着滑着就卡死了,Log里全是OutOfMemoryError。查了半天,发现是一个静态的Activity引用没释放。嗯,从那以后,我再也不敢小看任何一个引用。
3.1 内存泄漏的本质
Java里判断对象能不能回收,用的是可达性分析算法。从GC Roots出发,一路往下找引用链。如果一个对象从GC Roots出发找不到,那它就是不可达的,可以被回收。反过来,如果有一条引用链连着,那它就还活着。
泄漏的本质是什么?就是一个对象已经没用了,但GC Roots到它之间,还存在着一条引用路径。这条路径就是泄漏的元凶。
核心公式:
内存泄漏 = 无用对象 + 可达引用链
我见过不少开发者,一听到内存泄漏就紧张。其实你只要记住:长生命周期的对象,持有了短生命周期对象的引用,十有八九就是泄漏。
3.2 LeakCanary原理与使用
LeakCanary是Square公司开源的自动内存泄漏检测工具。说实话,它是我个人最常用的工具之一。集成简单,检测精准,还能直接告诉你泄漏的引用链。
3.2.1 工作原理
LeakCanary的核心逻辑其实不复杂:
- 它监听Activity和Fragment的生命周期。当onDestroy调用后,它认为这个页面该被回收了。
- 它用WeakReference包装目标对象,然后主动触发一次GC。
- 如果WeakReference.get()返回null,说明对象已被回收,万事大吉。
- 如果WeakReference.get()不为null,说明对象还活着。这时候它就会dump堆快照,分析引用链。
为什么会这样?说白了,LeakCanary就是在帮你做一件事:验证你认为该回收的对象,到底有没有被回收。
小提示:LeakCanary默认只在debug模式下工作。release包记得关掉,不然会影响性能。
3.2.2 集成与使用
集成方式很简单,在build.gradle里加一行依赖就行:
dependencies {
// debugImplementation 确保只在debug包生效
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
}
加完依赖,不需要任何初始化代码。LeakCanary会自动通过ContentProvider完成初始化。你只需要正常跑App,当发生泄漏时,它会自动弹出通知。
点开通知,你会看到完整的泄漏引用链。比如:
┬───
│ GC Root: System class
│
├─ android.app.ActivityThread class
│ Leaking: NO (a static class)
│ ↓ static ActivityThread.sCurrentActivity
│
├─ com.example.MainActivity instance
│ Leaking: YES (RefWatcher was watching this)
│ ↓ MainActivity.mHandler
│
├─ com.example.MyHandler instance
│ Leaking: YES (Handler is anonymous class)
│ ↓ MyHandler.this$0
│
╰→ com.example.MainActivity instance
Leaking: YES (RefWatcher was watching this)
这个引用链告诉你:ActivityThread的静态变量sCurrentActivity,引用了MainActivity,而MainActivity里的Handler又持有了外部类的引用,导致Activity无法释放。
注意:LeakCanary分析出来的引用链,是从GC Root到泄漏对象的完整路径。你修复泄漏,就是打断这条路径上的任意一环。
3.3 常见泄漏场景
我这些年排查过的泄漏,翻来覆去就那么几类。下面我一个个说,每个都附上避坑指南。
3.3.1 静态变量泄漏
静态变量的生命周期和Application一样长。如果你把一个Activity或者Context赋值给静态变量,那这个Activity就永远别想被回收了。
// 错误示例
public class LeakExample {
private static Context sContext;
public static void setContext(Context context) {
sContext = context; // 泄漏!Activity被静态变量持有
}
}
修复方案:能用ApplicationContext就别用ActivityContext。如果必须持有Activity引用,记得在不用的时候置null。
// 正确做法
public class SafeExample {
private static Context sContext;
public static void setContext(Context context) {
sContext = context.getApplicationContext(); // 使用ApplicationContext
}
public static void clear() {
sContext = null; // 主动释放
}
}
3.3.2 内部类泄漏
非静态内部类会隐式持有外部类的引用。如果你在Activity里定义了一个非静态内部类,并且把这个内部类的实例传给了某个长生命周期的对象,那Activity就泄漏了。
我曾经在一个项目里遇到过:一个网络请求的回调用了匿名内部类,回调里更新UI。网络请求是全局单例管理的,请求完成后回调还在,但Activity已经finish了。结果就是Activity泄漏,页面关不掉。
// 泄漏示例
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 匿名内部类隐式持有Activity引用
NetworkManager.getInstance().fetchData(new Callback() {
@Override
public void onSuccess(String data) {
// 更新UI
textView.setText(data);
}
});
}
}
修复方案:使用静态内部类 + WeakReference,或者在onDestroy时取消网络请求。
// 修复方案
public class MainActivity extends AppCompatActivity {
private static class SafeCallback implements Callback {
private WeakReference<MainActivity> activityRef;
SafeCallback(MainActivity activity) {
activityRef = new WeakReference<>(activity);
}
@Override
public void onSuccess(String data) {
MainActivity activity = activityRef.get();
if (activity != null && !activity.isFinishing()) {
activity.textView.setText(data);
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
NetworkManager.getInstance().fetchData(new SafeCallback(this));
}
}
3.3.3 Handler泄漏
Handler泄漏是内部类泄漏的一个特例,但太常见了,我单独拿出来说。
Handler发送的Message会持有Handler的引用。如果Message还没处理完,Activity就finish了,那Handler因为持有外部Activity的引用,导致Activity泄漏。
// 泄漏示例
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 处理消息
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 发送一个延迟消息
mHandler.sendMessageDelayed(Message.obtain(), 60000);
}
}
修复方案:使用静态内部类 + WeakReference,并在onDestroy时移除所有消息。
// 修复方案
public class MainActivity extends AppCompatActivity {
private static class SafeHandler extends Handler {
private WeakReference<MainActivity> activityRef;
SafeHandler(MainActivity activity) {
activityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = activityRef.get();
if (activity != null && !activity.isFinishing()) {
// 处理消息
}
}
}
private SafeHandler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler = new SafeHandler(this);
mHandler.sendMessageDelayed(Message.obtain(), 60000);
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null); // 移除所有消息
}
}
3.3.4 资源未关闭
BroadcastReceiver、Cursor、FileStream、数据库连接……这些资源用完了不关,也会造成泄漏。虽然不一定是内存泄漏,但会导致资源耗尽。
我记得有一次,一个同事在Activity里注册了BroadcastReceiver,但忘了在onDestroy里unregister。结果Activity被销毁了,Receiver还活着,每次广播过来都会回调一个已经销毁的Activity。嗯,这种问题用LeakCanary查不出来,但用Profiler一看就知道。
// 正确做法
public class MainActivity extends AppCompatActivity {
private BroadcastReceiver mReceiver;
@Override
protected void onResume() {
super.onResume();
IntentFilter filter = new IntentFilter("com.example.MY_ACTION");
registerReceiver(mReceiver, filter);
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mReceiver); // 必须取消注册
}
}
3.4 MAT与Android Studio Profiler实战
LeakCanary能帮你发现泄漏,但有时候它分析不出来,或者你想更深入地了解堆内存情况。这时候就需要手动分析了。
3.4.1 Android Studio Profiler
Profiler是Android Studio自带的性能分析工具。用它查内存泄漏,我一般这么操作:
- 打开Profiler,选择Memory面板。
- 反复进出目标页面,同时手动触发GC(点那个垃圾桶图标)。
- 观察内存曲线。如果每次进出后内存没有回落到基线,说明有泄漏。
- 点击Dump Java Heap,导出hprof文件。
导出后,你可以直接分析。但说实话,Android Studio自带的分析器功能有限。我更喜欢用MAT。
3.4.2 MAT实战
MAT是Eclipse Memory Analyzer的缩写。虽然名字带Eclipse,但它是个独立工具,专门分析Java堆快照。
使用步骤:
- 用Profiler导出hprof文件。
- 用Android SDK自带的hprof-conv工具转换格式:
hprof-conv input.hprof output.hprof - 用MAT打开转换后的文件。
打开后,我一般先看Histogram。它按类名列出所有对象实例的数量和占用大小。你找那些不该有这么多实例的类,比如Activity、Fragment、View等。
然后右键点击可疑类,选择Merge Shortest Paths to GC Roots,再选exclude all phantom/weak/soft etc. references。这样就能看到从GC Root到该对象的强引用链。
实战技巧:在Histogram里,输入Activity的类名,看看实例数量。如果已经退出了页面,但实例数量不为0,那肯定有泄漏。然后顺着引用链找,谁持有了这个Activity的引用。
3.5 知识体系总览
下面这张图,是我自己总结的内存泄漏知识体系。你把它存下来,排查问题时对着看,基本不会漏。
这张图把内存泄漏的检测、场景、分析和修复串在了一起。你排查问题时,按这个流程走,基本不会漏掉关键点。
我的习惯:每次开发完一个新功能,我都会用LeakCanary跑一遍。确保没有新增泄漏,再提交代码。这个习惯帮我避免了好几次线上OOM事故。
公众号:蓝海资料掘金营,微信deep3321