4. 内存泄漏实战(下):资源型泄漏与第三方库泄漏

好,咱们接着聊内存泄漏。上一章我们把静态集合、内部类这些常见的泄漏点都过了一遍。这一章,我重点说说资源型泄漏和第三方库的坑。说白了,就是那些你用了但没还回去的东西——系统资源、文件句柄、网络连接,它们一旦赖着不走,内存就炸了。

4.1 资源型泄漏:那些被你遗忘的“借条”

资源型泄漏,本质上就是你向系统借了资源,用完了没还。系统以为你还在用,就一直给你留着。你想想看,这跟借钱不还有啥区别?

4.1.1 BroadcastReceiver 未注销

这个坑我踩过不止一次。记得有一次线上反馈说App切后台再切回来,操作越来越卡。我一看内存,好家伙,Activity泄漏了十几个。查了半天,发现是某个Fragment里动态注册了BroadcastReceiver,但onDestroy里忘了调用unregisterReceiver。

为什么会泄漏?因为系统会持有这个Receiver的引用,而Receiver又持有Activity/Fragment的引用。GC一看,这引用链还连着,没法回收。

⚠️ 避坑指南
我曾经在项目里遇到过:一个同事在onResume里注册了Receiver,但onPause里没注销。结果用户频繁切换App,Receiver越积越多,最后直接OOM。所以我的习惯是:成对出现——register和unregister必须成对,最好写在onStart/onStop或者onResume/onPause里。
// 错误写法:只注册不注销
@Override
protected void onResume() {
    super.onResume();
    IntentFilter filter = new IntentFilter("com.example.MY_ACTION");
    registerReceiver(myReceiver, filter);
    // 忘了在onPause里unregister!
}

// 正确写法:成对出现
@Override
protected void onResume() {
    super.onResume();
    IntentFilter filter = new IntentFilter("com.example.MY_ACTION");
    registerReceiver(myReceiver, filter);
}

@Override
protected void onPause() {
    super.onPause();
    unregisterReceiver(myReceiver); // 必须注销
}

4.1.2 Cursor 未关闭

Cursor这玩意儿,说白了就是数据库查询结果的游标。你用ContentResolver.query查完数据,Cursor里存着查询结果。如果你不close它,它占用的内存和文件句柄就一直不释放。

我个人习惯是:用try-finally保证close。或者直接用Kotlin的use扩展函数,它自动帮你close。

// 错误写法:查完就忘
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
if (cursor != null) {
    while (cursor.moveToNext()) {
        // 处理数据
    }
    // 忘了close!
}

// 正确写法:try-finally保证关闭
Cursor cursor = null;
try {
    cursor = getContentResolver().query(uri, null, null, null, null);
    if (cursor != null) {
        while (cursor.moveToNext()) {
            // 处理数据
        }
    }
} finally {
    if (cursor != null) {
        cursor.close(); // 确保关闭
    }
}

// Kotlin推荐写法
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
    while (cursor.moveToNext()) {
        // 处理数据
    }
} // use会自动close

4.1.3 IO 流未关闭

IO流泄漏,说白了就是文件读完了没关。FileInputStream、FileOutputStream、BufferedReader这些,用完了必须close。不然文件句柄一直占着,系统能打开的文件数量是有限的,一旦超了,你就打不开新文件了。

我记得有一次做日志上传功能,用户反馈说上传几次之后App就卡死了。一查,原来是每次上传都new了一个FileInputStream,但没close。文件句柄泄漏,最后系统拒绝再打开新文件。

💡 我的习惯
所有IO操作,一律用try-with-resources(Java 7+)或者Kotlin的use。这样代码简洁,还不会忘。
// Java try-with-resources
try (FileInputStream fis = new FileInputStream(file);
     BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) {
    String line;
    while ((line = reader.readLine()) != null) {
        // 处理每一行
    }
} catch (IOException e) {
    e.printStackTrace();
} // 自动close

// Kotlin use
FileInputStream(file).bufferedReader().use { reader ->
    reader.lineSequence().forEach { line ->
        // 处理每一行
    }
}

4.2 第三方库泄漏:你以为它帮你管好了?

很多同学觉得,用了第三方库就不用操心内存了。其实不然。第三方库也是代码,也有bug。我见过太多因为第三方库使用不当导致的内存泄漏了。

4.2.1 Glide 图片加载库泄漏

Glide本身对内存管理做得很好,但如果你用错了,照样泄漏。最常见的情况是:在Activity/Fragment的onDestroy里没有清理Glide的请求

Glide默认会绑定Activity/Fragment的生命周期。但如果你在非UI线程或者自定义View里用Glide,它可能不会自动取消。这时候就需要手动清理。

// 正确做法:在onDestroy里清理
@Override
protected void onDestroy() {
    super.onDestroy();
    Glide.with(this).onDestroy(); // 清理当前页面的Glide请求
}

// 或者使用RequestManager
RequestManager requestManager = Glide.with(this);
requestManager.load(url).into(imageView);

// 在不需要时取消
requestManager.pauseRequests();
⚠️ 特别注意
如果你在自定义View里用Glide,记得在View detached from window时清理。我遇到过项目里自定义View持有Glide的RequestManager,View被移除后RequestManager还活着,导致图片一直加载,内存暴涨。

4.2.2 OkHttp 网络库泄漏

OkHttp泄漏,说白了就是Response Body没关闭。OkHttp的Response.body()返回的是一个流,你必须调用close()或者把整个body读完,否则连接不会释放回连接池。

我记得有一次做接口调试,发现每次请求后内存都涨一点,但不会降。查了半天,发现是某个同事在解析完数据后,忘了关闭Response Body。连接池里的连接越来越多,最后内存爆了。

// 错误写法:只取数据不关流
Response response = client.newCall(request).execute();
String data = response.body().string(); // 这里其实已经自动关闭了
// 但如果用response.body().byteStream(),就必须手动关

// 正确写法:确保body关闭
try (Response response = client.newCall(request).execute()) {
    if (response.isSuccessful()) {
        String data = response.body().string(); // string()内部会关闭
        // 处理data
    }
} // try-with-resources会自动关闭response

// 如果手动读取流
Response response = client.newCall(request).execute();
try (InputStream is = response.body().byteStream()) {
    // 读取流
} // 自动关闭

4.3 LeakCanary 源码分析与集成

LeakCanary,说白了就是内存泄漏的“照妖镜”。它能自动检测Activity、Fragment等对象的泄漏,并给出详细的引用链。我几乎每个项目都会集成它,省心省力。

4.3.1 集成方式

集成LeakCanary非常简单,两行代码搞定:

// build.gradle
dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
    releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:2.12'
}

// Application
public class MyApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)) {
            return; // 分析进程不初始化
        }
        LeakCanary.install(this);
    }
}
💡 我的习惯
debug包集成LeakCanary,release包用no-op。这样线上包不会有任何性能损耗,debug包又能实时检测泄漏。

4.3.2 源码核心逻辑

LeakCanary的原理,说白了就是:监听Activity/Fragment的onDestroy,然后过一会儿检查它是否被GC回收了。如果没回收,就认为泄漏了,然后dump堆栈分析引用链。

核心流程我用一张图来说明:

LeakCanary 核心检测流程 Activity onDestroy ObjectWatcher 记录弱引用 等待 5 秒(GC) 弱引用是否被回收? ✅ 已回收,正常 ❌ 未回收,泄漏 Dump Heap 分析 核心逻辑:监听 onDestroy → 弱引用记录 → 等待 GC → 检查回收状态

核心源码其实不复杂。LeakCanary内部维护了一个ObjectWatcher,它持有被监测对象的弱引用。当Activity的onDestroy被调用时,ObjectWatcher会记录这个Activity的弱引用。然后过5秒,它检查这个弱引用是否被回收了。如果没回收,就触发Heap Dump,然后分析引用链。

// LeakCanary 核心逻辑简化版
public class ObjectWatcher {
    private final Set<KeyedWeakReference> watchedReferences = new CopyOnWriteArraySet<>();

    public void watch(Object object, String name) {
        // 记录弱引用
        KeyedWeakReference reference = new KeyedWeakReference(object, name);
        watchedReferences.add(reference);
        // 5秒后检查
        checkRetainedObjects();
    }

    private void checkRetainedObjects() {
        // 触发GC
        System.gc();
        // 等待GC完成
        // 检查弱引用是否被回收
        for (KeyedWeakReference ref : watchedReferences) {
            if (ref.get() != null) {
                // 没回收,泄漏了!
                dumpHeapAndAnalyze(ref);
            }
        }
    }
}

4.3.3 如何分析LeakCanary报告

LeakCanary检测到泄漏后,会生成一个通知。点开通知,你会看到详细的引用链。比如:

┬───
│ GC Root: System class
│
├─ android.app.ActivityThread class
│    Leaking: NO (ActivityThread is a system class)
│    ↓ static ActivityThread.sCurrentActivityThread
│
├─ android.app.ActivityThread instance
│    Leaking: NO (ActivityThread is a system class)
│    ↓ ActivityThread.mActivities
│
├─ android.util.ArrayMap instance
│    Leaking: NO (ActivityThread is a system class)
│    ↓ ArrayMap.mArray
│
├─ java.lang.Object[] array
│    Leaking: NO (ActivityThread is a system class)
│    ↓ Object[].[0]
│
├─ android.app.ActivityThread$ActivityClientRecord instance
│    Leaking: NO (ActivityThread is a system class)
│    ↓ ActivityClientRecord.activity
│
├─ com.example.MainActivity instance
│    Leaking: YES (Activity#mDestroyed is true)
│    ↓ MainActivity.mHandler
│
├─ android.os.Handler instance
│    Leaking: YES (Handler is not static)
│    ↓ Handler.mCallback
│
├─ com.example.MainActivity$1 instance
│    Leaking: YES (anonymous class implements Callback)
│    ↓ MainActivity$1.this$0
│
╰→ com.example.MainActivity instance
     Leaking: YES (RefWatcher was watching this)

这个引用链告诉我们:MainActivity泄漏了,原因是内部匿名类Handler持有外部Activity的引用。说白了,就是Handler里的Callback是匿名内部类,隐式持有外部Activity的引用,导致Activity无法被回收。

🔍 分析技巧
看LeakCanary报告,重点关注两点:
1. GC Root:谁引用了泄漏对象?通常是系统类、静态变量、线程等。
2. 引用链:从GC Root到泄漏对象的路径。路径上的每个节点,你都要问自己:这个引用能不能去掉?能不能改成弱引用?

4.4 本章小结

资源型泄漏和第三方库泄漏,说白了就是“借了没还”。BroadcastReceiver、Cursor、IO流,这些都是系统资源,用完了必须还。Glide、OkHttp这些第三方库,虽然帮你做了很多,但也不是万能的,该手动清理的时候别偷懒。

LeakCanary是个好工具,我建议每个项目都集成。它能帮你快速定位泄漏点,省去你手动排查的时间。但记住,工具只是辅助,真正避免泄漏,还是要靠良好的编码习惯。

嗯,这一章就到这里。下一章我们聊聊如何用MAT和Android Studio Profiler分析内存问题,到时候我会分享一些我实际项目中的排查案例。


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