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

UI 测试中的内存泄漏检测

在 UI 测试中运行内存泄漏检测意味着你可以在新泄漏合并到代码库之前,在持续集成中自动检测到内存泄漏。

测试环境检测

在调试版本中,LeakCanary 持续寻找被保留的实例,在监视的对象被保留 5 秒后冻结 VM 以获取堆转储,然后在后台线程中执行分析并使用通知报告结果。这种行为不适合 UI 测试,因此当 JUnit 在运行时 classpath 中时,LeakCanary 会自动禁用(参见 测试环境检测)。

入门

LeakCanary 提供了一个专门用于在 UI 测试中检测泄漏的 artifact

androidTestImplementation 'com.squareup.leakcanary:leakcanary-android-instrumentation:2.14'
// You still need to include the LeakCanary artifact in your app:
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'

然后你可以在测试中的任何时候调用 LeakAssertions.assertNoLeaks() 来检查泄漏

class CartTest {

  @Test
  fun addItemToCart() {
    // ...
    LeakAssertions.assertNoLeaks()
  }
}

如果检测到被保留的实例,LeakCanary 将转储和分析堆。如果发现应用泄漏,LeakAssertions.assertNoLeaks() 将抛出 NoLeakAssertionFailedError

leakcanary.NoLeakAssertionFailedError: Application memory leaks were detected:
====================================
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS

┬───
│ GC Root: System class
│
├─ com.example.MySingleton class
│    Leaking: NO (a class is never leaking)
│    ↓ static MySingleton.leakedView
│                         ~~~~~~~~~~
├─ android.widget.TextView instance
│    Leaking: YES (View.mContext references a destroyed activity)
│    ↓ TextView.mContext
╰→ com.example.MainActivity instance
     Leaking: YES (Activity#mDestroyed is true)
====================================
  at leakcanary.AndroidDetectLeaksAssert.assertNoLeaks(AndroidDetectLeaksAssert.kt:34)
  at leakcanary.LeakAssertions.assertNoLeaks(LeakAssertions.kt:21)
  at com.example.CartTest.addItemToCart(TuPeuxPasTest.kt:41)

混淆的 instrumentation 测试

当针对混淆的发布版本运行 instrumentation 测试时,LeakCanary 类会分布在测试 APK 和主 APK 中。不幸的是,Android Gradle Plugin 中存在一个 错误,导致运行测试时出现运行时崩溃,因为主 APK 中的代码发生了变化,而测试 APK 中使用这些代码的部分没有相应更新。如果你遇到此问题,设置 Keeper 插件 应该可以解决它。

测试规则

你可以使用 DetectLeaksAfterTestSuccess 测试规则在测试结束时自动调用 LeakAssertions.assertNoLeaks()

class CartTest {
  @get:Rule
  val rule = DetectLeaksAfterTestSuccess()

  @Test
  fun addItemToCart() {
    // ...
  }
}

你也可以在单个测试中多次调用 LeakAssertions.assertNoLeaks()

class CartTest {
  @get:Rule
  val rule = DetectLeaksAfterTestSuccess()

  // This test has 3 leak assertions (2 in the test + 1 from the rule).
  @Test
  fun addItemToCart() {
    // ...
    LeakAssertions.assertNoLeaks()
    // ...
    LeakAssertions.assertNoLeaks()
    // ...
  }
}

跳过内存泄漏检测

使用 @SkipLeakDetection 来禁用 LeakAssertions.assertNoLeaks() 调用

class CartTest {
  @get:Rule
  val rule = DetectLeaksAfterTestSuccess()

  // This test will not perform any leak assertion.
  @SkipLeakDetection("See issue #1234")
  @Test
  fun addItemToCart() {
    // ...
    LeakAssertions.assertNoLeaks()
    // ...
    LeakAssertions.assertNoLeaks()
    // ...
  }
}

你可以使用 标签 来标识每个 LeakAssertions.assertNoLeaks() 调用,并仅禁用这些调用的一部分

class CartTest {
  @get:Rule
  val rule = DetectLeaksAfterTestSuccess(tag = "EndOfTest")

  // This test will only perform the second leak assertion.
  @SkipLeakDetection("See issue #1234", "First Assertion", "EndOfTest")
  @Test
  fun addItemToCart() {
    // ...
    LeakAssertions.assertNoLeak(tag = "First Assertion")
    // ...
    LeakAssertions.assertNoLeak(tag = "Second Assertion")
    // ...
  }
}

可以通过调用 HeapAnalysisSuccess.assertionTag 来检索标签,它们也会在堆分析结果元数据中报告

====================================
METADATA

Please include this in bug reports and Stack Overflow questions.

Build.VERSION.SDK_INT: 23
...
assertionTag: Second Assertion

测试规则链

// Example test rule chain
@get:Rule
val rule = RuleChain.outerRule(LoginRule())
  .around(ActivityScenarioRule(CartActivity::class.java))
  .around(LoadingScreenRule())

如果你使用测试规则链,DetectLeaksAfterTestSuccess 规则在该链中的位置可能很重要。例如,如果你使用 ActivityScenarioRule,它会在测试结束时自动完成 activity,将 DetectLeaksAfterTestSuccess 放在 ActivityScenarioRule 周围,将在 activity 被销毁后检测泄漏,从而检测到任何 activity 泄漏。但这样 DetectLeaksAfterTestSuccess 将不会检测那些在 activity 销毁时消失的 fragment 泄漏。

@get:Rule
val rule = RuleChain.outerRule(LoginRule())
  // Detect leaks AFTER activity is destroyed
  .around(DetectLeaksAfterTestSuccess(tag = "AfterActivityDestroyed"))
  .around(ActivityScenarioRule())
  .around(LoadingScreenRule())

相反,如果你将 ActivityScenarioRule 放在 DetectLeaksAfterTestSuccess 周围,已销毁的 activity 泄漏将不会被检测到,因为在泄漏断言规则运行时 activity 仍然存在,但可能会检测到更多 fragment 泄漏。

@get:Rule
val rule = RuleChain.outerRule(LoginRule())
  .around(ActivityScenarioRule(CartActivity::class.java))
  // Detect leaks BEFORE activity is destroyed
  .around(DetectLeaksAfterTestSuccess(tag = "BeforeActivityDestroyed"))
  .around(LoadingScreenRule())

为了检测所有泄漏,最佳选择是将 DetectLeaksAfterTestSuccess 规则设置两次,分别在 ActivityScenarioRule 规则之前和之后。

// Detect leaks BEFORE and AFTER activity is destroyed
@get:Rule
val rule = RuleChain.outerRule(LoginRule())
  .around(DetectLeaksAfterTestSuccess(tag = "AfterActivityDestroyed"))
  .around(ActivityScenarioRule(CartActivity::class.java))
  .around(DetectLeaksAfterTestSuccess(tag = "BeforeActivityDestroyed"))
  .around(LoadingScreenRule())

RuleChain.detectLeaksAfterTestSuccessWrapping() 是一个实现此目的的辅助函数

// Detect leaks BEFORE and AFTER activity is destroyed
@get:Rule
val rule = RuleChain.outerRule(LoginRule())
  // The tag will be suffixed with "Before" and "After".
  .detectLeaksAfterTestSuccessWrapping(tag = "ActivitiesDestroyed") {
    around(ActivityScenarioRule(CartActivity::class.java))
  }
  .around(LoadingScreenRule())

自定义 assertNoLeaks()

LeakAssertions.assertNoLeaks() 将调用委托给全局的 DetectLeaksAssert 实现,默认情况下是 AndroidDetectLeaksAssert 的一个实例。你可以通过调用 DetectLeaksAssert.update(customLeaksAssert) 来更改 DetectLeaksAssert 实现。

AndroidDetectLeaksAssert 实现会在检测到被保留的实例时执行堆转储,分析堆,然后将结果传递给 HeapAnalysisReporter。默认的 HeapAnalysisReporterNoLeakAssertionFailedError.throwOnApplicationLeaks(),它会在检测到应用泄漏时抛出 NoLeakAssertionFailedError

你可以提供一个自定义实现,在测试失败之前将堆分析结果上传到中心位置

val throwingReporter = NoLeakAssertionFailedError.throwOnApplicationLeaks()

DetectLeaksAssert.update(AndroidDetectLeaksAssert(
  heapAnalysisReporter = { heapAnalysis ->
    // Upload the heap analysis result
    heapAnalysisUploader.upload(heapAnalysis)
    // Fail the test if there are application leaks
    throwingReporter.reportHeapAnalysis(heapAnalysis)
  }
))