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

代码秘籍

本页包含代码秘籍,可根据您的需求定制 LeakCanary。浏览各章节标题,烹饪您自己的美味佳肴!也别忘了查看常见问题

Bug

如果您认为可能缺少某个秘籍,或者不确定使用当前 API 是否能实现您想要的功能,请提交一个 issue。您的反馈有助于我们为整个社区改进 LeakCanary。

监视具有生命周期的对象

LeakCanary 的默认配置将自动监视 Activity、Fragment、Fragment View 和 ViewModel 实例。

在您的应用中,可能还有其他具有生命周期的对象,例如服务、Dagger 组件等。使用 AppWatcher.objectWatcher 来监视应进行垃圾回收的实例。

class MyService : Service {

  // ...

  override fun onDestroy() {
    super.onDestroy()
    AppWatcher.objectWatcher.watch(
      watchedObject = this,
      description = "MyService received Service#onDestroy() callback"
    )
  }
}

配置

LeakCanary 有一个适用于大多数应用的默认配置。您也可以根据需要进行定制。LeakCanary 配置由两个单例对象(AppWatcherLeakCanary)持有,并且可以随时更新。大多数开发者在其 debug Application 类中配置 LeakCanary。

class DebugExampleApplication : ExampleApplication() {

  override fun onCreate() {
    super.onCreate()
    AppWatcher.config = AppWatcher.config.copy(watchFragmentViews = false)
  }
}

信息

在您的 src/debug/java 文件夹中创建一个 debug Application 类。别忘了也在 src/debug/AndroidManifest.xml 中注册它。

要在运行时自定义保留对象的检测,请通过 AppWatcher.manualInstall() 指定您希望安装的观察器。

val watchersToInstall = AppWatcher.appDefaultWatchers(this)
  .filter { it !is FragmentAndViewModelWatcher }
AppWatcher.manualInstall(
  application = this,
  watchersToInstall = watchersToInstall
)

要自定义堆转储和分析,请更新 LeakCanary.config

LeakCanary.config = LeakCanary.config.copy(retainedVisibleThreshold = 3)

Java

在 Java 中,改用 LeakCanary.Config.Builder

LeakCanary.Config config = LeakCanary.getConfig().newBuilder()
   .retainedVisibleThreshold(3)
   .build();
LeakCanary.setConfig(config);

通过覆盖以下资源来配置 LeakCanary UI

禁用 LeakCanary

有时需要暂时禁用 LeakCanary,例如进行产品演示或运行性能测试时。您有不同的选择,具体取决于您想实现的目标。

  • 创建一个不包含 LeakCanary 依赖项的构建变体,请参阅为不同的产品变体设置 LeakCanary
  • 禁用堆转储和分析:LeakCanary.config = LeakCanary.config.copy(dumpHeap = false)
  • 隐藏泄漏显示 activity 的启动器图标:覆盖 R.bool.leak_canary_add_launcher_icon 或调用 LeakCanary.showLeakDisplayActivityLauncherIcon(false)

信息

当您将 LeakCanary.Config.dumpHeap 设置为 false 时,AppWatcher.objectWatcher 仍会跟踪保留的对象,当您将 LeakCanary.Config.dumpHeap 改回 true 时,LeakCanary 会查找这些对象。

LeakCanary 测试环境检测

默认情况下,LeakCanary 会在您的 classpath 中查找 org.junit.Test 类,如果找到,则会禁用自身以避免在测试中运行。但是,有些应用可能在其 debug classpath 中包含 JUnit(例如,使用 OkHttp 的 MockWebServer 时),因此我们提供了一种方法来定制用于确定应用是否在测试环境中运行的类。

<resources>
  <string name="leak_canary_test_class_name">assertk.Assert</string>
</resources>

在发布版本中统计保留的实例

com.squareup.leakcanary:leakcanary-android 依赖项只能用于 debug 构建。它依赖于 com.squareup.leakcanary:leakcanary-object-watcher-android,您可以在发布构建中使用它来跟踪和统计保留的实例。

在您的 build.gradle 文件中

dependencies {
  implementation 'com.squareup.leakcanary:leakcanary-object-watcher-android:2.14'
}

在您的泄漏报告代码中

val retainedInstanceCount = AppWatcher.objectWatcher.retainedObjectCount

LeakCanary 用于发布版本

我们不建议在发布版本中包含 LeakCanary,因为它可能会对您的客户体验产生负面影响。为避免在发布版本中意外包含 com.squareup.leakcanary:leakcanary-android 依赖项,如果 APK 不可调试,LeakCanary 会在初始化期间崩溃。您可能有充分的理由创建一个包含 LeakCanary 的非可调试构建,例如用于 QA 构建。如果需要,可以通过覆盖 bool/leak_canary_allow_in_non_debuggable_build 资源来禁用崩溃检查,例如在 res/values 下创建一个文件,其内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <bool name="leak_canary_allow_in_non_debuggable_build">true</bool>
</resources>

Android TV

LeakCanary 可以在 Android TV 设备(FireTV、Nexus player、Nvidia Shield、MiBox 等)上工作,无需额外设置。但是,您需要注意几件事:

  • Android TV 没有通知。当对象被保留以及泄漏分析完成时,LeakCanary 将显示 Toast 消息。您还可以查看 Logcat 获取更多详细信息。
  • 由于没有通知,手动触发堆转储的唯一方法是将应用置于后台。
  • 在 API 26+ 设备上有一个 bug,它会阻止显示泄漏的 activity 出现在应用列表中。作为一种变通方法,LeakCanary 会在堆转储分析后在 Logcat 中打印一条 adb shell 命令,用于启动泄漏列表 activity。
    adb shell am start -n "com.your.package.name/leakcanary.internal.activity.LeakLauncherActivity"
    
  • 一些 Android TV 设备每个应用进程可用的内存非常少,这可能会影响 LeakCanary。在单独的进程中运行 LeakCanary 分析可能会在这种情况下有所帮助。

图标和标签

显示泄漏的 activity 带有默认图标和标签,您可以通过在应用中提供 R.mipmap.leak_canary_iconR.string.leak_canary_display_activity_label 来更改。

res/
  mipmap-hdpi/
    leak_canary_icon.png
  mipmap-mdpi/
    leak_canary_icon.png
  mipmap-xhdpi/
    leak_canary_icon.png
  mipmap-xxhdpi/
    leak_canary_icon.png
  mipmap-xxxhdpi/
    leak_canary_icon.png
   mipmap-anydpi-v26/
     leak_canary_icon.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="leak_canary_display_activity_label">MyLeaks</string>
</resources>

匹配已知的库泄漏

LeakCanary.Config.referenceMatchers 设置为一个列表,该列表基于 AndroidReferenceMatchers.appDefaults 构建。

class DebugExampleApplication : ExampleApplication() {

  override fun onCreate() {
    super.onCreate()
    LeakCanary.config = LeakCanary.config.copy(
        referenceMatchers = AndroidReferenceMatchers.appDefaults +
            AndroidReferenceMatchers.staticFieldLeak(
                className = "com.samsing.SomeSingleton",
                fieldName = "sContext",
                description = "SomeSingleton has a static field leaking a context.",
                patternApplies = {
                  manufacturer == "Samsing" && sdkInt == 26
                }
            )
    )
  }
}

忽略特定的 Activity 或 Fragment 类

有时,第三方库会提供自己的 activity 或 fragment,其中包含许多导致这些特定第三方 activity 和 fragment 泄漏的 bug。您应该努力推动该库修复其内存泄漏,因为它直接影响您的应用。话虽如此,在它们修复之前,您有两种选择:

  1. 将特定的泄漏添加为已知库泄漏(请参阅匹配已知的库泄漏)。当检测到这些泄漏时,LeakCanary 将运行并将其报告为已知库泄漏。
  2. 禁用 LeakCanary 自动监视 activity 或 fragment(例如 AppWatcher.config = AppWatcher.config.copy(watchActivities = false)),然后手动将对象传递给 AppWatcher.objectWatcher.watch

识别泄漏对象并标记对象

class DebugExampleApplication : ExampleApplication() {

  override fun onCreate() {
    super.onCreate()
    val addEntityIdLabel = ObjectInspector { reporter ->
      reporter.whenInstanceOf("com.example.DbEntity") { instance ->
        val databaseIdField = instance["com.example.DbEntity", "databaseId"]!!
        val databaseId = databaseIdField.value.asInt!!
        labels += "DbEntity.databaseId = $databaseId"
      }
    }

    val singletonsInspector =
      AppSingletonInspector("com.example.MySingleton", "com.example.OtherSingleton")

    val mmvmInspector = ObjectInspector { reporter ->
      reporter.whenInstanceOf("com.mmvm.SomeViewModel") { instance ->
        val destroyedField = instance["com.mmvm.SomeViewModel", "destroyed"]!!
        if (destroyedField.value.asBoolean!!) {
          leakingReasons += "SomeViewModel.destroyed is true"
        } else {
          notLeakingReasons += "SomeViewModel.destroyed is false"
        }
      }
    }

    LeakCanary.config = LeakCanary.config.copy(
        objectInspectors = AndroidObjectInspectors.appDefaults +
            listOf(addObjectIdLabel, singletonsInspector, mmvmInspector)
    )
  }
}

在单独的进程中运行 LeakCanary 分析

LeakCanary 在您的主应用进程中运行。LeakCanary 2 经过优化,可在分析时保持低内存使用,并在优先级为 Process.THREAD_PRIORITY_BACKGROUND 的后台线程中运行。如果您发现 LeakCanary 仍然使用了太多内存或影响了应用进程的性能,您可以将其配置为在单独的进程中运行分析。

您只需将 leakcanary-android 依赖项替换为 leakcanary-android-process

dependencies {
  // debugImplementation 'com.squareup.leakcanary:leakcanary-android:${version}'
  debugImplementation 'com.squareup.leakcanary:leakcanary-android-process:${version}'
}

您可以调用 LeakCanaryProcess.isInAnalyzerProcess 来检查您的 Application 类是否正在 LeakCanary 进程中创建。这在配置 Firebase 等库时很有用,这些库可能在意外的进程中运行时崩溃。

为不同的产品变体设置 LeakCanary

您可以设置 LeakCanary 在应用的特定产品变体中运行。例如,创建

android {
  flavorDimensions "default"
  productFlavors {
    prod {
      // ...
    }
    qa {
      // ...
    }
    dev {
      // ...
    }
  }
}

然后,为您想要启用 LeakCanary 的变体定义自定义配置

android {
  // ...
}
configurations {
    devDebugImplementation {}
}

现在您可以为该配置添加 LeakCanary 依赖项

dependencies {
  devDebugImplementation "com.squareup.leakcanary:leakcanary-android:${version}"
}

从堆转储中提取元数据

LeakCanary.Config.metadataExtractor 从堆转储中提取元数据。然后元数据在 HeapAnalysisSuccess.metadata 中可用。LeakCanary.Config.metadataExtractor 默认为 AndroidMetadataExtractor,但您可以替换它以从 hprof 中提取额外的元数据。

例如,如果您想在堆分析报告中包含应用版本名称,您需要先将其存储在内存中(例如,在静态字段中),然后才能在 MetadataExtractor 中检索它。

class DebugExampleApplication : ExampleApplication() {

  companion object {
    @JvmStatic
    lateinit var savedVersionName: String
  }

  override fun onCreate() {
    super.onCreate()

    val packageInfo = packageManager.getPackageInfo(packageName, 0)
    savedVersionName = packageInfo.versionName

    LeakCanary.config = LeakCanary.config.copy(
        metadataExtractor = MetadataExtractor { graph ->
          val companionClass =
            graph.findClassByName("com.example.DebugExampleApplication")!!

          val versionNameField = companionClass["savedVersionName"]!!
          val versionName = versionNameField.valueAsInstance!!.readAsJavaString()!!

          val defaultMetadata = AndroidMetadataExtractor.extractMetadata(graph)

          mapOf("App Version Name" to versionName) + defaultMetadata
        })
  }
}

在混淆的应用中使用 LeakCanary

如果开启了混淆,则泄漏跟踪将会被混淆。通过使用 LeakCanary 提供的去混淆 Gradle 插件,可以自动对泄漏跟踪进行去混淆。

您必须在根 build.gradle 文件中添加插件依赖项

buildscript {
  dependencies {
    classpath 'com.squareup.leakcanary:leakcanary-deobfuscation-gradle-plugin:${version}'
  }
}

然后,您需要在您的应用(或库)特定的 build.gradle 文件中应用和配置插件

apply plugin: 'com.android.application'
apply plugin: 'com.squareup.leakcanary.deobfuscation'

leakCanary {
  // LeakCanary needs to know which variants have obfuscation turned on
  filterObfuscatedVariants { variant ->
    variant.name == "debug"
  }
}

现在您可以在混淆的应用上运行 LeakCanary,泄漏跟踪将自动去混淆。

重要:切勿在 release 变体上使用此插件。此插件会复制混淆映射文件并将其放入 .apk 中,因此如果您在发布构建上使用它,则混淆将变得毫无意义,因为可以使用映射文件轻松进行代码去混淆。

警告:R8(Google 的 Proguard 替代品)现在可以理解 Kotlin 语言构造,但副作用是映射文件会变得非常大(几十兆字节)。这意味着包含复制的映射文件的 .apk 大小也会增加。这是不在 release 变体上使用此插件的另一个原因。

在 JVM 应用中检测泄漏

虽然 LeakCanary 设计用于在 Android 上开箱即用,但经过一些配置,它可以在任何 JVM 上运行。

将 ObjectWatcher 和 Shark 依赖项添加到您的构建文件中

dependencies {
  implementation 'com.squareup.leakcanary:leakcanary-object-watcher:2.14'
  implementation 'com.squareup.leakcanary:shark:2.14'
}

定义一个 HotSpotHeapDumper 来转储堆

import com.sun.management.HotSpotDiagnosticMXBean
import java.lang.management.ManagementFactory

object HotSpotHeapDumper {
  private val mBean: HotSpotDiagnosticMXBean by lazy {
    val server = ManagementFactory.getPlatformMBeanServer()
    ManagementFactory.newPlatformMXBeanProxy(
        server,
        "com.sun.management:type=HotSpotDiagnostic",
        HotSpotDiagnosticMXBean::class.java
    )
  }

  fun dumpHeap(fileName: String) {
    mBean.dumpHeap(fileName, LIVE)
  }

  private const val LIVE = true
}

定义一个 JvmHeapAnalyzer 来分析对象被保留时的堆,并将结果打印到控制台

import leakcanary.GcTrigger
import leakcanary.ObjectWatcher
import leakcanary.OnObjectRetainedListener
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale.US

class JvmHeapAnalyzer(private val objectWatcher: ObjectWatcher) :
    OnObjectRetainedListener {

  private val fileNameFormat = SimpleDateFormat(DATE_PATTERN, US)

  override fun onObjectRetained() {
    GcTrigger.Default.runGc()
    if (objectWatcher.retainedObjectCount == 0) {
      return
    }
    val fileName = fileNameFormat.format(Date())
    val hprofFile = File(fileName)

    println("Dumping the heap to ${hprofFile.absolutePath}")
    HotSpotHeapDumper.dumpHeap(hprofFile.absolutePath)

    val analyzer = HeapAnalyzer(
        OnAnalysisProgressListener { step ->
          println("Analysis in progress, working on: ${step.name}")
        })

    val heapDumpAnalysis = analyzer.analyze(
        heapDumpFile = hprofFile,
        leakingObjectFinder = KeyedWeakReferenceFinder,
        computeRetainedHeapSize = true,
        objectInspectors = ObjectInspectors.jdkDefaults
    )
    println(heapDumpAnalysis)
  }
  companion object {
    private const val DATE_PATTERN = "yyyy-MM-dd_HH-mm-ss_SSS'.hprof'"
  }
}

创建一个 ObjectWatcher 实例并将其配置为监视对象 5 秒,然后通知 JvmHeapAnalyzer 实例

val scheduledExecutor = Executors.newSingleThreadScheduledExecutor()
val objectWatcher = ObjectWatcher(
    clock = Clock {
      System.currentTimeMillis()
    },
    checkRetainedExecutor = Executor { command ->
      scheduledExecutor.schedule(command, 5, SECONDS)
    }
)

val heapAnalyzer = JvmHeapAnalyzer(objectWatcher)
objectWatcher.addOnObjectRetainedListener(heapAnalyzer)

将您期望被垃圾回收的对象(例如,已关闭的资源)传递给 ObjectWatcher 实例

objectWatcher.watch(
    watchedObject = closedResource,
    description = "$closedResource is closed and should be garbage collected"
)

如果您最终在 JVM 上使用了 LeakCanary,社区肯定会从您的经验中受益,所以请不要犹豫 告诉我们

PackageManager.getLaunchIntentForPackage() 返回 LeakLauncherActivity

LeakCanary 添加了一个主 activity,其具有 Intent#CATEGORY_LAUNCHER 类别。PackageManager.getLaunchIntentForPackage() 首先在 Intent#CATEGORY_INFO 类别中查找主 activity,然后查找 Intent#CATEGORY_LAUNCHER 类别中的主 activity。PackageManager.getLaunchIntentForPackage() 返回在您应用的合并清单中匹配的第一个 activity。如果您的应用依赖于 PackageManager.getLaunchIntentForPackage(),您有两种选择:

  • Intent#CATEGORY_INFO 添加到您的主 activity Intent 过滤器中,以便它首先被选中。这是 Android 文档推荐的做法。
  • 通过将 leak_canary_add_launcher_icon 布尔资源设置为 false 来禁用 leakcanary 启动器 activity。