Android 内存泄漏
- 内存泄漏的原因
- 常见的内存泄漏与解决方法
- 检测内存泄漏
认识内存泄漏
根本原因就是当一个对象理应被回收的时候,因为在某个地方持有该对象的引用,导致它不能正常被 JVM 回收,而停留在堆内存中。
在 Android 中具体的例子大部分是:当我们关闭了一个 Activity/Fragment 时,此时 Activity/Fragment 变为不可见,内存中的实例也应当被回收,假如这时候还有别的对象实例强引用了 Activity/Fragment 的实例导致一直无法回收,则出现了内存泄漏。
关于 JAVA 的内存分配和回收,引用一段:
Java内存划分为栈、堆、方法区等区域,其中栈保存的是方法的局部变量,随方法起随方法灭,不需要GC;
堆保存所有对象的实例和数组,是GC和泄露的重点区;
方法区保存的是类信息、常量、静态变量等静态信息,也需要GC。
堆内存的回收中,判断对象存活的算法有引用计数算法和可达性分析算法,引用计数算法无法解决对象间循环引用的问题,虚拟机通常采用可达性分析算法。
常见的垃圾回收算法有:标记 - 清除法、复制算法、标记 - 整理法、分代回收算法。
常见的垃圾回收器种类有:Serial、ParNew、Parallel Scavenge等。
关于强引用:
对应的常用概念还有软引用、弱引用。
强引用特点是 JVM 即使内存耗尽也不会去自动回收该对象:
Object o = new Object();//强引用
而软引用在内存不足时,会被 JVM 回收:
Staff bean = new Staff();//仅用于创建软引用的实例
SoftReference<Staff> staffSR = new SoftReference<Staff>(bean);
//实际调用
String StaffID = staffSR.get().getId();
弱引用的使用和软引用类似,不同的是当 JVM 触发了 GC 时,不管当前内存空间足够与否,都会被回收:
Staff bean = new Staff();//仅用于创建弱引用的实例
WeakReference<Staff> staffWR = new WeakReference<Staff>(bean);
//实际调用
String StaffID = staffSR.get().getId();
内存泄漏的实例
日常的内存泄漏其实追溯到最后还是间接或直接的持有了 Activity/Fragment 的实例,但是有些确实防不胜防。一不注意就会踩雷。
- 单例造成的内存泄露
这个就是老生常谈了,因为单例的生命周期同应用一样长,又常有构造方法中需要传入 context 的情况。
这时候就要注意,如果传入了 Activity 的 Context,一不小心就会使这个单例持有了 Activity 的实例而出现内存泄漏。
所以这种情况下,一般用 Application 的 Context 来构造单例对象的实例。
((Activity)context).getApplicationContext();
首先要回忆内部类的特性,内部类可以访问外部类的实例。
对于 JVM 来说,内部类和外部类其实是两个不同的类,正常来说两个类之间调用方法自然是通过两者的实例调用。
而非静态内部类之所以能访问外部类的方法,关键在于非静态内部类会默认隐性的持有外部类的实例。
但静态的内部类则不会持有外部类的实例。
相关的典型代码:
public class TestActivity extends Activity {
private final Handler mTestHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//TODO
}
}
}
这时候的 Handler 是非静态的,所以该实例持有了 TestActivity 的实例。
当调用:
//延时发送消息
mTestHandler.postDelayed(new Runnable() {
@Override
public void run() {
//TODO
}
}, 1000);
如果不清楚 Handler 的原理和源码的是时候去补习一下了。这时候 Handler 发送了 Message 实例,而这个 Message 实例引用了 Handler 实例,同时 Message 被主线程的 Looper 引用,此时的引用链:
Looper -> Message -> Handler -> Activity
这个时候即使调用 ((Activity)context).finish()
Activity 也不能被回收。
所以上面的情况下要将 Handler 转化成静态类即可,或者继承 Handler 将隐性引用的 Activity 实例改为弱引用:
private static class MyHandler extends Handler {
private final WeakReference<TestActivity> mActivity;
public MyHandler(TestActivity activity) {
mActivity = new WeakReference<TestActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
TestActivity activity = mActivity.get();
if (activity != null) {
//TODO
}
}
}
和这个问题类似的还有线程造成的泄漏,两者的原因都是因为用了非静态的内部类:
public class ThreadTestActivity extends Activity {
private class MyThread extends Thread {
@Override
public void run() {
super.run();
//TODO
}
}
-
关于非静态内部类还有另一个注意点:
如果在非静态内部类中创建了一个静态的实例,这个操作相当于上面说的用 Activity 的实例来创建单例对象的实例:静态实例会一直持有 Activity(外部类) 的实例。 -
资源未关闭导致
Cursor、InputStream/OutputStream、File 等资源文件,如果仅仅是在使用结束后将引用赋为 null,而不调用关闭的方法,还是有可能会造成内存泄漏。
特别是 Cursor,使用数据库时如果没有处理好可能会出现 OOM 或 Could not allocate CursorWindow,就是因为没有关闭导致:
Cursor c ;
//TODO get Cursor
try {
c = query();
//TODO something
c.close();
//如果 try 中抛出异常,上面的 cursor.close() 很大可能不会执行
} catch (Exception e) {
} finally{
//如果没有 finally 块会容易出错
if (c != null) {
c.close();
}
}
类似的还有广播、EventBus 等需要注册的同时也要记得在合适的地方注销。
这次负责的其中一个项目是运行在固定的设备上:Android 4.4.2 的大平板上,所以这个坑是实实在在的踩下去了。
AlertDialog 中的监听回调都是靠 Handler 来实现的:
/*
* 以下代码出自 android-23 - android - app - Dialog
*/
public void show() {
if (mShowing) {
if (mDecor != null) {
if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
}
mDecor.setVisibility(View.VISIBLE);
}
return;
}
//省略部分代码
try {
mWindowManager.addView(mDecor, l);
mShowing = true;
sendShowMessage();
} finally {
}
}
@Override
public void dismiss() {
if (Looper.myLooper() == mHandler.getLooper()) {
dismissDialog();
} else {
mHandler.post(mDismissAction);
}
}
而我们使用 Dialog 时 .setPositiveButton()
、.setNegativeButton()
、.setNeutralButton()
都以非静态内部类的形式实现,而这些点击的事件同样是通过 Handler 回调,这就会将这些内部类包装成一个 Message 传给 Dialog,这个 Message 就强引用了 Activity 的实例。
而 Dialog 在使用这些 Message 的时候会拷贝一个对象而不是用原来的对象:
private void sendDismissMessage() {
if (mDismissMessage != null) {
// Obtain a new message so this dialog can be re-used
Message.obtain(mDismissMessage).sendToTarget();
}
}
也就是说后面使用的是 Message 的拷贝。所以原来的 Message 从没有被发送,因此不会被回收,所以永久保存着它的内容,直到发生垃圾回收。
所以现在的引用链:
Thread(CookieSyncManager) -> Message -> AlertDialog$3(OnDismissListener) -> AlertDialog -> Activity
当然 5.0 以上已经解决了这个问题,所以这个案例可以看一看就过。
检测内存泄漏
不管用什么工具和方法,检测是否内存泄漏的方法都依靠 heap dump 文件。
heap dump 文件是一个二进制文件,保存了某一时刻 JVM 堆中对象使用情况,就是生成文件时的 Java 堆栈的快照。我们可以选择用 Heap Analyzer 分析 heap dump 文件,看哪些对象占用了太多的堆栈空间,或者哪些对象应该被回收却还在内存中。而 Leakcanary 等框架可以帮助我们省去一部分的工作。
- Leakcanary
build.gradle 中添加如下依赖:
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
}
在 Application 初始化:
public class MyApplication extends Application {
private RefWatcher mWatcher;
@Override
public void onCreate() {
super.onCreate();
mWatcher = LeakCanary.install(this);//此时已经可以检测到 Activity 的内存泄漏
}
public static RefWatcher getWatcher(Context context){
return ((MyApplication)context.getApplicationContext()).mWatcher;
}
}
检测 Fragment :
@Override
public void onDestroy() {
super.onDestroy();
//watch() 也可以传入别的对象实例来检测是否泄露
MyApplication.getWatcher().watch(this);
}
Leakcanary 的 install(application)
相当于在 Activity 的 onDestroy()
中调用 watch(this)
。
其原理是在 Activity / Fragment 销毁后,先手动促发一次 GC(系统 GC 并不会在销毁后立刻发生)
如果 watch(Object bean)
中传入的 bean 实例依然存在在内存中,则 dump heap 到本地,
dump 完成后启动 HeapAnalyzerService 服务读取本地 dump下来的文件,使用 HAHA 库进行分析。
如果检测到内存泄漏,将结果返回给 DisplayLeakService 服务,并且弹窗显示通知。
- Android Studio
在不想额外的依赖 Leakcanary 等框架是,利用 AS 自带的 Monitor 同样可以检测内存泄漏,只是多了一些步骤。
Android Monitor -> Monitor
Monitors从左到右:
Initiate GC // 手动触发 GC。
Jump java heap// 获取 hprof 分析文件。
Start Allocation Tracking// 开始分配追踪。
在 Jump java heap 之前还是要记得触发一次 GC。点击后会生成一个 后缀为 hprof
的文件,在 AS 打开:
右边的 Analyzer Tasks
打开分析窗口:
右上角开始,在 Results 可以看到分析结果。