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的核心逻辑其实不复杂:

  1. 它监听Activity和Fragment的生命周期。当onDestroy调用后,它认为这个页面该被回收了。
  2. 它用WeakReference包装目标对象,然后主动触发一次GC。
  3. 如果WeakReference.get()返回null,说明对象已被回收,万事大吉。
  4. 如果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自带的性能分析工具。用它查内存泄漏,我一般这么操作:

  1. 打开Profiler,选择Memory面板。
  2. 反复进出目标页面,同时手动触发GC(点那个垃圾桶图标)。
  3. 观察内存曲线。如果每次进出后内存没有回落到基线,说明有泄漏。
  4. 点击Dump Java Heap,导出hprof文件。

导出后,你可以直接分析。但说实话,Android Studio自带的分析器功能有限。我更喜欢用MAT。

3.4.2 MAT实战

MAT是Eclipse Memory Analyzer的缩写。虽然名字带Eclipse,但它是个独立工具,专门分析Java堆快照。

使用步骤:

  1. 用Profiler导出hprof文件。
  2. 用Android SDK自带的hprof-conv工具转换格式:hprof-conv input.hprof output.hprof
  3. 用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 手动GC + 观察内存 静态变量 内部类 / Handler 资源未关闭 Android Studio Profiler MAT (Memory Analyzer) 打断引用链 生命周期对齐 弱引用 / 静态内部类 核心:长生命周期对象 → 短生命周期对象 = 泄漏

这张图把内存泄漏的检测、场景、分析和修复串在了一起。你排查问题时,按这个流程走,基本不会漏掉关键点。

我的习惯:每次开发完一个新功能,我都会用LeakCanary跑一遍。确保没有新增泄漏,再提交代码。这个习惯帮我避免了好几次线上OOM事故。


公众号:蓝海资料掘金营,微信deep3321