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内部维护了一个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