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
。默认的 HeapAnalysisReporter
是 NoLeakAssertionFailedError.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)
}
))