跳到内容
🤔 文档问题? 报告编辑

修复内存泄漏

内存泄漏是一种编程错误,它导致应用程序保留了对不再需要的对象的引用。在代码的某个地方,存在一个应该被清除但未清除的引用。

按照这 4 个步骤来修复内存泄漏

  1. 找到泄漏路径。
  2. 缩小嫌疑引用范围。
  3. 找到导致泄漏的引用。
  4. 修复泄漏。

LeakCanary 帮你完成前两个步骤。最后两个步骤取决于你自己!

1. 找到泄漏路径

泄漏路径从垃圾收集根到被保留对象的最佳强引用路径的简称,即在内存中持有对象从而阻止其被垃圾回收的引用路径。

例如,我们将一个 helper 单例存储在一个静态字段中

class Helper {
}

class Utils {
  public static Helper helper = new Helper();
}

我们告诉 LeakCanary 该单例实例预期会被垃圾回收

AppWatcher.objectWatcher.watch(Utils.helper)

该单例的泄漏路径如下所示

┬───
│ GC Root: Local variable in native code
│
├─ dalvik.system.PathClassLoader instance
│    ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│    ↓ Object[].[43]
├─ com.example.Utils class
│    ↓ static Utils.helper
╰→ java.example.Helper

让我们分解一下!在顶部,一个 PathClassLoader 实例被一个垃圾收集 (GC) 根持有,更具体地说,是一个本地代码中的局部变量。GC 根是总是可达的特殊对象,即它们不能被垃圾回收。GC 根主要有 4 种类型

  • 局部变量,属于线程的栈。
  • 活跃 Java 线程的实例。
  • 系统类,永不卸载。
  • 本地引用,由本地代码控制。
┬───
│ GC Root: Local variable in native code
│
├─ dalvik.system.PathClassLoader instance

├─ 开头的行表示一个 Java 对象(可以是类、对象数组或实例),而以 │ ↓ 开头的行表示对下一行 Java 对象的引用。

PathClassLoader 有一个 runtimeInternalObjects 字段,它是对一个 Object 数组的引用

├─ dalvik.system.PathClassLoader instance
│    ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array

Object 数组中位置 43 的元素是对 Utils 类的引用。

├─ java.lang.Object[] array
│    ↓ Object[].[43]
├─ com.example.Utils class

╰→ 开头的行表示泄漏对象,即传递给 AppWatcher.objectWatcher.watch() 的对象。

Utils 类有一个静态的 helper 字段,它是对泄漏对象的引用,即 Helper 单例实例

├─ com.example.Utils class
│    ↓ static Utils.helper
╰→ java.example.Helper instance

2. 缩小嫌疑引用范围

泄漏路径是一系列引用组成的路径。最初,该路径中的所有引用都被怀疑是导致泄漏的原因,但 LeakCanary 可以自动缩小嫌疑引用范围。为了理解这意味着什么,我们手动走一遍这个过程。

这里有一个糟糕的 Android 代码示例

class ExampleApplication : Application() {
  val leakedViews = mutableListOf<View>()
}

class MainActivity : Activity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main_activity)

    val textView = findViewById<View>(R.id.helper_text)

    val app = application as ExampleApplication
    // This creates a leak, What a Terrible Failure!
    app.leakedViews.add(textView)
  }
}

LeakCanary 会生成一个看起来像这样的泄漏路径

┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│    ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│    ↓ ExampleApplication.leakedViews
├─ java.util.ArrayList instance
│    ↓ ArrayList.elementData
├─ java.lang.Object[] array
│    ↓ Object[].[0]
├─ android.widget.TextView instance
│    ↓ TextView.mContext
╰→ com.example.leakcanary.MainActivity instance

以下是阅读该泄漏路径的方法

FontsContract 类是一个系统类(参见 GC Root: System class),它有一个 sContext 静态字段,该字段引用了一个 ExampleApplication 实例,该实例有一个 leakedViews 字段,该字段引用了一个 ArrayList 实例,该实例引用了一个数组(作为 ArrayList 实现的后备数组),该数组包含一个元素,该元素引用了一个 TextView,该 TextView 有一个 mContext 字段,该字段引用了一个已销毁的 MainActivity 实例。

LeakCanary 使用 ~~~ 下划线高亮显示所有被怀疑导致此泄漏的引用。最初,所有引用都是嫌疑对象

┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│    ↓ static FontsContract.sContext
│                           ~~~~~~~~
├─ com.example.leakcanary.ExampleApplication instance
│    Leaking: NO (Application is a singleton)
│    ↓ ExampleApplication.leakedViews
│                         ~~~~~~~~~~~
├─ java.util.ArrayList instance
│    ↓ ArrayList.elementData
│                ~~~~~~~~~~~
├─ java.lang.Object[] array
│    ↓ Object[].[0]
│               ~~~
├─ android.widget.TextView instance
│    ↓ TextView.mContext
│               ~~~~~~~~
╰→ com.example.leakcanary.MainActivity instance

然后,LeakCanary 对泄漏路径中对象的状态生命周期进行推断。在 Android 应用中,Application 实例是一个永不被垃圾回收的单例,因此它永远不会泄漏(Leaking: NO (Application is a singleton))。由此,LeakCanary 得出结论,泄漏不是由 FontsContract.sContext 引起的(移除了相应的 ~~~)。以下是更新后的泄漏路径

┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│    ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│    Leaking: NO (Application is a singleton)
│    ↓ ExampleApplication.leakedViews
│                         ~~~~~~~~~~~
├─ java.util.ArrayList instance
│    ↓ ArrayList.elementData
│                ~~~~~~~~~~~
├─ java.lang.Object[] array
│    ↓ Object[].[0]
│               ~~~
├─ android.widget.TextView instance
│    ↓ TextView.mContext
│               ~~~~~~~~
╰→ com.example.leakcanary.MainActivity instance

TextView 实例通过其 mContext 字段引用了已销毁的 MainActivity 实例。视图不应在其上下文的生命周期结束后仍然存活,因此 LeakCanary 知道此 TextView 实例正在泄漏(Leaking: YES (View.mContext references a destroyed activity)),从而得出泄漏不是由 TextView.mContext 引起的(移除了相应的 ~~~)。以下是更新后的泄漏路径

┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│    ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│    Leaking: NO (Application is a singleton)
│    ↓ ExampleApplication.leakedViews
│                         ~~~~~~~~~~~
├─ java.util.ArrayList instance
│    ↓ ArrayList.elementData
│                ~~~~~~~~~~~
├─ java.lang.Object[] array
│    ↓ Object[].[0]
│               ~~~
├─ android.widget.TextView instance
│    Leaking: YES (View.mContext references a destroyed activity)
│    ↓ TextView.mContext
╰→ com.example.leakcanary.MainActivity instance

总结一下,LeakCanary 检查泄漏路径中对象的状态,以确定这些对象是否正在泄漏(Leaking: YES 对比 Leaking: NO),并利用这些信息来缩小嫌疑引用范围。你可以提供自定义的 ObjectInspector 实现,以改进 LeakCanary 在你的代码库中的工作方式(参见识别泄漏对象和标记对象)。

3. 找到导致泄漏的引用

在前面的示例中,LeakCanary 将嫌疑引用范围缩小到 ExampleApplication.leakedViewsArrayList.elementDataObject[].[0]

┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│    ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│    Leaking: NO (Application is a singleton)
│    ↓ ExampleApplication.leakedViews
│                         ~~~~~~~~~~~
├─ java.util.ArrayList instance
│    ↓ ArrayList.elementData
│                ~~~~~~~~~~~
├─ java.lang.Object[] array
│    ↓ Object[].[0]
│               ~~~
├─ android.widget.TextView instance
│    Leaking: YES (View.mContext references a destroyed activity)
│    ↓ TextView.mContext
╰→ com.example.leakcanary.MainActivity instance

ArrayList.elementDataObject[].[0]ArrayList 的实现细节,ArrayList 实现中不太可能存在 bug,因此导致泄漏的引用是唯一剩下的引用:ExampleApplication.leakedViews

4. 修复泄漏

找到导致泄漏的引用后,你需要弄清楚该引用是什么,它何时应该被清除以及为何没有被清除。有时原因很明显,就像前面的示例一样。有时你需要更多信息才能弄清楚。你可以添加标签,或直接探索 hprof 文件(参见如何深入挖掘泄漏路径之外的信息?)。

警告

内存泄漏不能通过将强引用替换为弱引用来修复。这是一种试图快速解决内存问题的常见方法,但它永远无效。导致引用保留时间超过必要的 bug 仍然存在。除此之外,这还会引入更多 bug,因为某些对象现在会比预期更早被垃圾回收。这也使得代码更难维护。

接下来做什么?使用代码示例根据你的需要自定义 LeakCanary!