前言


作为 Android 开发者,内存泄漏是我们无法忽视的问题。但在实际开发过程中,内存泄漏又是很隐蔽的。如何定义内存泄漏,内存泄漏是怎么产生的,该如何避免内存泄漏,这些将是本文讨论的重点。

何为内存泄漏


关于内存泄漏,我是这样理解的:

无用对象因被其他正在使用的实例持有引用,导致其占用的内存无法正常地被回收。

即使知道内存泄漏的存在,我们可能还是会不自觉地写出导致内存泄漏的代码(更恰当的表述应该是“无意识的对象保持”)。
如果发生了内存泄漏,无用对象占用的内存无法释放,应用可用内存减少,性能下降,更严重的时候可能还会触发 内存溢出错误(Out Of Memory Error)

内存泄漏是如何产生的


在了解内存泄漏产生的原因之前,还需通过一些概念来帮助理解:

Java 内存区域划分

我们知道一般的 Java 程序运行在 Java 虚拟机 (JVM) 中,JVM 的内存区域主要分为五个部分:

  • 程序计数器:控制代码流程,记录当前线程执行的位置。
  • Java 虚拟机栈:存放局部变量表、操作数栈、动态链接、方法出口信息。
  • 本地方法栈:与 Java 虚拟机栈实现的功能类似,只不过存放的是本地方法的信息。
  • 堆:存放对象的内存空间,程序运行时这部分内存由 JVM 管理分配。
  • 方法区:存放常量、静态变量、类信息等。

介绍完 Java 的内存区域划分,我们再通过 Java 对象的创建过程进行总结。

Java 对象的创建和使用

举个例子:

public class Car {
public static int WHEEL_COUNT;
public Car() {}
}
...
public static void main(String[] args) {
Car sportsCar = new Car();
sportsCar.start();
}

上面这段代码中声明了一个 Car 类型的变量 sportsCar 存放在虚拟机栈中。在通过 new 指令创建一个 Car 对象之前,JVM 会先检查内存中是否加载了 Car.class ,如果没加载,则会将 Car.class 加载到方法区中,保存类的信息、static 关键字修饰的实例对象和 final 关键字修饰的实例对象等。然后在堆中开辟一块内存区域用于保存 new Car() 这个对象,并将一个指向它的强引用存储在 sportsCar 中。当执行 sportsCar.start() 方法时,实际上是 sportsCar 通过引用操控原对象调用的。

Java 垃圾回收机制

Java 作为一种具有垃圾回收功能 (Garbage Collection,GC) 的语言,开发者不需要调用函数来释放内存,内存的回收都由 GC 来完成。虽然 Android 使用的 Dalvik/ART 虚拟机与一般的 JVM 不同,但 GC 策略还是通过标记清理(Mark & Sweep)回收算法实现:

how gc works
  • Mark 阶段:
    从根节点集合开始遍历,将可强引用到达根节点的对象被标记为存活。
  • Sweep 阶段:
    回收那些没有被标记存活的对象所占用的内存。

其中,可以作为根节点的包括但不限于:① 栈(虚拟机栈/本地方法栈)中局部变量引用的对象;② 方法区中类静态变量引用的对象;③ 方法区中常量引用的对象; ④ 运行中的线程。

此时,当一个对象已经不再需要但仍被持有引用,导致所占用的资源无法被 GC 回收,这样的情况下内存泄漏就产生了。

解决内存泄漏的方法


导致内存泄漏的根本原因:
一个变量或常量持有一个对象的强引用,且其生命周期比对象的长,致使对象占用的内存无法回收。

解决方法:

  • 手动解除强引用;
  • 使用弱引用或软引用代替强引用。

这里有必要提一下 Java 中的四种引用类型:

  • 强引用:默认引用类型,比如之前的例子 Car sportsCar = new Car(); 就是变量 sportsCar 持有一个 Car 类型对象的强引用。在 GC 时,如果对象存在强引用就不能被回收;
  • 软引用:在 GC 时,如果对象只存在软引用,只在内存不足的情况下该对象才会被回收;
  • 弱引用:在 GC 时,如果对象只存在弱引用,该对象会被回收;
  • 虚引用:只存在虚引用的对象可能会在任何时候被 GC 回收。

因此使用弱引用或软引用可以有效地解决内存泄漏问题。

具体案例分析


静态变量

当应用程序启动时,系统会创建进程加载虚拟机。如果一个类被使用,虚拟机就会加载该类,这个类的静态变量等信息也将被分配到内存。一般在进程结束后类被卸载的情况下,静态变量占用的内存才能得到释放。所以静态变量的生命周期相当于整个应用程序的生命周期。
举一个简单的例子:

public class PreferencesHelper {
private static SharedPreferences sSharedPreferences;
private static volatile PreferencesHelper sInstance;
private PreferencesHelper(Context context) {
sSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
}
public static PreferencesHelper getInstance(Context context) {
if (sInstance == null) {
synchronized (PreferencesHelper.class) {
if (sInstance == null) {
sInstance = new PreferencesHelper(context);
}
}
}
return sInstance;
}
...
}

PreferencesHelper 是一个单例模式的工具类,方便调用系统的 SharedPreferences 。当我们传入 Activity 的 Context 作为参数时,PreferencesHelper 的实例将会一直持有 Activity 的强引用直到进程结束。如果此时屏幕旋转,当前 Activity 需要被销毁重建,但是又被静态变量持有强引用而无法被销毁。

解决的方法很简单,使用 Application 的 Context 作为参数传入就可以了,因为 Application 的生命周期等同于应用程序的生命周期。PreferencesHelper 的实例持有 Application 的强引用,也不需要每次调用时传入其他 Activity 的 Context 了。

非静态内部类

Java 中的非静态内部类会隐式持有外部类实例的强引用,如果使用不当则非常容易导致内存泄漏。例如在使用 Handler 的时候,继承 Handler 实现一个 SimpleHandler 类:

public class SimpleActivity extends AppCompatActivity {
private SimpleHandler mHandler = new SimpleHandler();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_simple);
mHandler.obtainMessage().sendToTarget();
}
private class SimpleHandler extends Handler {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
...
}
}
}

其中 SimpleHandler 就属于 SimpleActivity 的非静态内部类,SimpleHandler 的实例 mHandler 隐式持有外部类对象 this 的强引用。
在 Android 消息处理机制中,Looper 、MessageQueue 和 Handler 是不可或缺的三部分:

handler

Looper 在主线程启动时初始化,并在内部创建一个消息队列 MessageQueue 来存放消息,Looper 会不断地从 MessageQueue 中取出消息,如果队列中没消息便阻塞。Handler 负责发送消息,并将消息插入消息队列中,每条消息除了可能有 Message 和 Runnable 外,还包含了 Handler 实例对象的强引用。Looper 在取出消息后会通过这个 Handler 实例对象的引用调用 dispatchMessage(Message msg) 方法将消息处理掉。

当 Activity 生命周期结束时,仍然存在持有该 Activity 强引用的 Message 于消息队列中没被消费,Activity 的实例对象便不能被销毁。

解决的方法很简单,使用静态内部类 + 弱引用:

public class SimpleActivity extends AppCompatActivity {
private SimpleHandler mHandler = new SimpleHandler(this);
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_simple);
mHandler.obtainMessage().sendToTarget();
}
private static class SimpleHandler extends Handler {
private final WeakReference<SimpleActivity> mReference;
private SimpleHandler(SimpleActivity activity) {
mReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
...
}
}
}

将 Handler 设为静态内部类后不会隐式持有外部类的强引用,传入弱引用后也不会妨碍 GC 将 Activity 对象占用的内存回收。

有时候我们还会写出这样的代码:

private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
...
}
};

上面的 mHandler 是一个匿名内部类的实例,也属于非静态内部类的一种。实现匿名内部类时应当注意,在其生命周期结束时,是否存在其他变量持有匿名内部类对象的强引用,阻止 GC 回收。

异步线程

在执行一些耗时操作时,我们通常会开启一个线程去处理:

public class SimpleActivity extends AppCompatActivity {
private Thread mThread;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_simple);
mThread = new Thread() {
@Override
public void run() {
super.run();
while (循环条件) {
...
}
}
};
mThread.start();
}
}

mThread 其实也属于 SimpleActivity 的匿名内部类,持有外部类的强引用。当 Activity 销毁时线程仍未结束就会产生内存泄漏,所以需要手动管理线程的生命周期:

public class SimpleActivity extends AppCompatActivity {
private Thread mThread;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_simple);
mThread = new Thread() {
@Override
public void run() {
super.run();
while (循环条件 && !isInterrupted()) { // 加入条件判断线程是否中断
...
}
}
};
mThread.start();
}
@Override
protected void onStop() {
super.onStop();
if (mThread != null) {
mThread.interrupt(); // 在 Activity 停止时中断线程
}
}
}

同理,使用 Runnable 处理循环操作时也要注意在 Activity 销毁时及时中断线程。

在 Android 中,系统还提供了 AsyncTask 类方便使用者轻松地异步处理数据并更新到 UI 界面上。不过 AsyncTask 并不会随着 Activity 的销毁而销毁,它会一直执行 doInBackground() 方法直到方法执行结束。好在 AsyncTask 还提供了一个 cancel(boolean mayInterruptIfRunning) 方法取消提交的任务,只是这方法并非在任何情况下都起作用:
cancel() 方法传入一个 boolean 类型的参数,意为“是否可以打断正在执行的任务”。
如果调用的是 cancel(false)doInBackground() 的执行不受影响,只不过任务结束时调用的是 onCancelled() 方法而不是 onPostExecute() 方法。这显然不是我们想要的结果,Activity 销毁时 AsyncTask 仍然在执行无用的操作。
如果调用的是 cancel(true) ,我们还需要做一些处理确保 AsyncTask 正确地被取消:

public class SimpleActivity extends AppCompatActivity {
private AsyncTask<String, Void, Void> mTask;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_simple);
mTask = new AsyncTask<String, Void, Void>() {
@Override
protected Void doInBackground(String... strings) {
while (循环条件 && !isCancelled()) { // 加入条件判断任务是否取消
...
}
return null;
}
};
mTask.execute("do something");
}
@Override
protected void onStop() {
super.onStop();
if (mTask != null) {
mTask.cancel(true); // 在 Activity 停止时取消正在执行的任务
}
}
}

doInBackground() 的循环方法中加入 isCancelled() 判断,mTask 调用 cancel(true) 时可以使任务尽早结束。如果 doInBackground() 执行的不是循环方法,还可以使用静态内部类 + 弱引用的方式避免内存泄漏发生。

属性动画

使用属性动画不当而导致内存泄漏的原因和异步线程一样。在 Activity / View 生命周期结束时,如果动画还在执行也会使其一直持有强引用直到动画结束,特别是在执行循环动画的情况下,Activity / View 的对象将无法得到释放。因此我们需要在 Activity / View 销毁时取消动画的执行。

在 Activity 中:

private ValueAnimator mAnimator;
...
@Override
protected void onStop() {
super.onStop();
if (mAnimator != null) {
mAnimator.cancel();
mAnimator = null;
}
}

在自定义 View 中:

private ValueAnimator mAnimator;
...
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAnimator != null) {
mAnimator.cancel();
mAnimator = null;
}
}

监听器

为了更新 UI 界面,我们通常会实现一个 Listener 方便监听数据变化从而实时刷新界面,举个例子:

public class SimpleActivity extends AppCompatActivity implements DownloadUtils.DownloadListener {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_simple);
DownloadUtils.getInstance().setListener(this);
}
@Override
public void onStart(String id) {
}
@Override
public void onPause(String id) {
}
@Override
public void onFinish(String id) {
}
@Override
public void onFail(String id, int errorCode) {
}
}

DownloadUtils 是一个下载工具类,使用单例模式实现,其作用是下载文件并通过监听器通知界面更新 UI 。SimpleActivity 实现了 DownloadUtils.DownloadListener 接口,DownloadUtils 的实例对象通过调用 setListener(DownloadListener listener) 方法持有 Activity 的强引用。如果此时退出 Activity 或者屏幕旋转,SimpleActivity 的实例对象依然无法被回收。

解决办法便是为 DownloadUtils 添加一个 removeListener(DownloadListener listener) 方法,在 Activity 销毁之前解除引用:

@Override
protected void onStop() {
super.onStop();
DownloadUtils.getInstance().removeListener(this);
}

集合

在使用 Collection 和 Map 这两大类集合的时候要注意,特别是在其生命周期较长的情况下(例如:某工具类里使用 List 类型的实例作为成员变量)。如果通过 add() 等方法持有外部类对象的强引用,在外部类对象销毁时又没有及时清除,集合内还维护着这些对象的过期引用。所以一旦外部类对象的生命周期结束,我们就要调用集合的 remove() 等方法清除这些引用。

结语


在很多情况下,发生内存泄漏的原因都是持有强引用变量的生命周期大于原对象的生命周期,致使本该被销毁的对象不能被 GC 回收。但我们也不必刻意去清空每个变量的引用,清空对象引用应该是一直例外,而不是规范行为。对于生命周期长的实例对象,只需要注意其是否持有其他生命周期较短对象的强引用,在对象生命周期结束时清除即可。

参考链接