java内存泄漏-java内存泄漏的原因
作者| Lily Chen 译者 | 平川企划 | 万家内存泄漏是Android应用中一个非常严重的问题。 本文介绍 Dropbox 如何处理内存泄漏。
当应用程序为一个对象分配内存,但该对象在不再使用时没有被释放时,就会发生内存泄漏。 随着时间的推移,泄漏的内存会累积,导致应用程序性能不佳甚至崩溃。 泄漏可能发生在任何程序和平台上,但由于 Activity 生命周期的复杂性,泄漏在 Android 应用程序中尤为普遍。 最新的 Android 模式(如 ViewModel 和 LifecycleObserver)可以帮助避免内存泄漏,但如果您遵循旧模式或不知道要注意什么,则很容易错过错误。
1 引用长期运行服务的常见示例
片段引用引用长期运行服务的活动
在这种情况下,我们有一个标准设置,其中 Activity 持有对长期运行服务的引用java内存泄漏,然后 Fragment 及其视图持有对 Activity 的引用。 例如,假设一个活动以某种方式创建了对其子片段的引用。 然后,只要 Activity 还活着,Fragment 也会继续存在。 那么Fragment的onDestroy和Activity的onDestroy之间就存在内存泄漏。
Fragment 永远不会被再次使用,但它会一直在内存中
长时间运行的服务引用片段视图
另一方面,如果服务获得对 Fragment 视图的引用怎么办?
首先,视图现在将在整个服务期间保持活动状态。 此外,因为视图持有对其父活动的引用,活动现在也会泄漏。
只要服务存在,FragmentView 和 Activity 都会浪费内存
2 检测内存泄漏
现在,我们已经了解了内存泄漏是如何发生的。 让我们讨论如何检测它们。 显然,第一步是检查您的应用程序是否会由于 OutOfMemoryError 而崩溃。 除非单个屏幕占用的内存超过手机的可用内存,否则某处一定存在内存泄漏。
这种方法只告诉你问题是什么,而不是根本原因。 内存泄漏可能发生在任何地方,记录的崩溃并不指向泄漏,而是指向最终显示内存使用量超过限制的屏幕。
您可以检查所有面包屑控件,看看它们是否有一些相似之处,但罪魁祸首很可能不容易识别。 让我们研究其他选项。
泄漏金丝雀
最好的工具之一是 LeakCanary,这是一个用于 Android 的内存泄漏检测库。 我们只需要在我们的构建中添加一个 build.gradle 文件依赖。 下次我们安装并运行我们的应用程序时,LeakCanary 将与它一起运行。 当我们在应用程序中导航时,LeakCanary 偶尔会暂停以转储内存并提供检测到的泄漏痕迹。
#:~:text=LeakCanary%20is%20a%20memory%20leak,developers%20dramatically%20reduce%20OutOfMemoryError%20crashes.?fileGuid=dg5RuSiDPDkmicBU
这个工具比我们以前的方法要好得多。 但这个过程仍然是手动的,每个开发人员只有他们亲身遇到的内存泄漏的本地副本。 我们可以做得更好!
LeakCanary 和 Bugsnag
LeakCanary 提供了一个非常方便的代码配方,用于将发现的泄漏上传到 Bugsnag。 我们可以像跟踪应用程序中的任何其他警告或崩溃一样跟踪内存泄漏。 我们甚至可以更进一步,使用 Bugsnag Integration 将其连接到 Jira 等项目管理软件,以获得更好的可见性和问责制。
#uploading-to-bugsnag?fileGuid=dg5RuSiDPDkmicBU
Bugsnag 连接到 Jira
LeakCanary 和集成测试
另一种提高自动化的方法是将 LeakCanary 与 CI 测试连接起来。 同样,我们有一个代码配方。
#running-leakcanary-in-instrumentation-tests?fileGuid=dg5RuSiDPDkmicBU
以下内容来自官方文档:
LeakCanary 提供了一个专门用于检测 UI 测试泄漏的小部件,它提供了一个等待测试完成的运行监听器,如果测试成功,它会查找持久化对象,如果需要则触发堆转储并执行分析。
请注意,LeakCanary 会减慢测试速度,因为它会在每次侦听的测试完成时转储堆。 在我们的例子中,由于我们的选择性测试和分片设置,额外的时间可以忽略不计。
最终,就像 CI 上的任何其他构建或测试失败一样,内存泄漏会暴露出来,错误跟踪信息也会被记录下来。
在 CI 上运行 LeakCanary 帮助我们学习了更好的编码模式,尤其是在任何代码投入生产之前涉及到新库时。 比如我们在用MvRx测试的时候,就发现了这个漏洞:
Test failed because application memory leaks were detected: ==================================== HEAP ANALYSIS RESULT ==================================== 4 APPLICATION LEAKS References underlined with "~~~" are likely causes. Learn more at https://squ.re/leaks. 198449 bytes retained by leaking objects Signature: 6bf2ba80511dcb6ab9697257143e3071fca4 ┬─── │ GC Root: System class
│ ├─ com.airbnb.mvrx.mocking.MockableMavericks class
│ Leaking: NO (a class is never leaking)
│ ↓ static MockableMavericks.mockStateHolder
│ ~~~~~~~~~~~~~~~
├─ com.airbnb.mvrx.mocking.MockStateHolder instance
│ Leaking: UNKNOWN
│ ↓ MockStateHolder.delegateInfoMap
│ ~~~~~~~~~~~~~~~
├─ java.util.LinkedHashMap instance
│ Leaking: UNKNOWN
│ ↓ LinkedHashMap.header
│ ~~~~~~
├─ java.util.LinkedHashMap$LinkedEntry instance
│ Leaking: UNKNOWN
│ ↓ LinkedHashMap$LinkedEntry.prv
│ ~~~
├─ java.util.LinkedHashMap$LinkedEntry instance
│ Leaking: UNKNOWN
│ ↓ LinkedHashMap$LinkedEntry.key
│ ~~~
╰→ com.dropbox.product.android.dbapp.photos.ui.view.PhotosFragment instance
Leaking: YES (ObjectWatcher was watching this because com.dropbox.product.android.dbapp.photos.ui.view.PhotosFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
key = 391c9051-ad2c-4282-9279-d7df13d205c3
watchDurationMillis = 7304
retainedDurationMillis = 2304 198427 bytes retained by leaking objects
Signature: d1c9f9707034dd15604d8f2e63ff3bf3ecb61f8
事实证明,在编写测试时,我们没有正确清理测试。 添加几行代码可以避免泄漏:
fun teardown() {
scenario.close()
val holder = MockableMavericks.mockStateHolder
holder.clearAllMocks()
}
您可能会想:既然这种内存泄漏只发生在测试中,修复它真的那么重要吗? 好吧,这取决于你! 与代码检查一样,泄漏检测可以告诉您何时存在代码异味或错误的编码模式。
它可以帮助工程师编写更健壮的代码——在这种情况下,我们知道 clearAllMocks()。 泄漏的严重程度以及是否必须修复,都是工程师可以做出的决定。
对于我们不想运行泄漏检测的测试,我们编写了一个简单的注释:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface SkipLeakDetection {
/**
* The reason why the test should skip leak detection.
*/
String value();
}
我们的类重写了 LeakCanary 的 FailOnLeakRunListener():
override fun skipLeakDetectionReason(description: Description): String? {
return when {
description.getAnnotation(SkipLeakDetection::class.java) != null ->
"is annotated with @SkipLeakDetection"
description.testClass.isAnnotationPresent(SkipLeakDetection::class.java) ->
"class is annotated with @SkipLeakDetection"
else -> null
}
}
单个测试或整个测试类可以使用此注释来跳过泄漏检测。
3 修复内存泄漏
我们现在已经讨论了查找和暴露内存泄漏的各种方法。 下面,我们讨论如何真正理解和修复它们。
LeakCanary 提供的泄漏跟踪是诊断泄漏最有用的工具。 本质上,泄漏跟踪会打印出与泄漏对象关联的引用链,并解释为什么将其视为泄漏。
LeakCanary 有关于如何阅读和使用泄漏跟踪器的很好的文档,因此无需在此重复。 相反,让我们回顾一下我自己经常处理的两种类型的内存泄漏。
看法
我们经常看到声明为类级变量的视图:private TextView myTextView; 或者java内存泄漏,现在更多的 Android 代码正在用 Kotlin 编写:private lateinit var myTextView: textview - 如此常见以至于我们没有意识到这些会导致内存泄漏。
除非您在 Fragment 的 onDestroyView 中删除对这些字段的引用(您不能对 lateinit 变量执行此操作),否则对这些视图的引用将在 Fragment 的整个生命周期内存在,而不是 Fragment 视图的生命周期,因为它们应该存在在循环中。
导致内存泄漏的最简单的场景之一是:我们在 FragmentA 上。 我们导航到 FragmentB,现在 FragmentA 在堆栈上。 FragmentA 没有被销毁,但是 FragmentA 的视图被销毁了。 现在不再需要绑定到 FragmentA 的生命周期的任何视图,但保留在内存中。
在大多数情况下,这些泄漏足够小,不会导致任何性能问题或崩溃。 但是对于保存对象和数据、图像、视图/数据绑定等的视图,我们更有可能遇到麻烦。
因此,如果可能,请避免将视图存储在类级变量中,或者确保在 onDestroyView 中正确清理它们。
说到视图/数据绑定,Android 的视图绑定文档明确告诉我们:必须清除字段以防止泄漏。 他们提供的代码片段建议我们执行以下操作:
private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
_binding = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
每个 Fragment 中有很多样板代码(另外,避免使用 !!,因为如果变量为 null,这将抛出 KotlinNullPointerException。改用显式 null 处理。)我们解决这个问题的方法是创建一个 ViewBindingHolder(和DataBindingHolder),Fragment可以实现如下:
interface ViewBindingHolder<B : ViewBinding> {
var binding: B?
// Only valid between onCreateView and onDestroyView.
fun requireBinding() = checkNotNull(binding)
fun requireBinding(lambda: (B) -> Unit) {
binding?.let {
lambda(it)
}}
/**
* Make sure to use this with Fragment.viewLifecycleOwner
*/
fun registerBinding(binding: B, lifecycleOwner: LifecycleOwner) {
this.binding = binding
lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
owner.lifecycle.removeObserver(this)
this@ViewBindingHolder.binding = null
}
})
}
}
interface DataBindingHolder<B : ViewDataBinding> : ViewBindingHolder<B>
这为 Fragment 提供了一种简单干净的方法:
临时泄漏
这些泄漏只会持续很短的时间。 特别是,我们遇到了由 EditTextView 异步任务引起的泄漏。 异步任务的持续时间仅比 LeakCanary 的默认等待时间长,因此即使内存已正确快速释放,也会报告泄漏。
如果您怀疑存在瞬时泄漏,检查的一个好方法是使用 Android Studio 的内存分析器。
在探查器中启动会话后,您可以逐步重现泄漏,但在转储堆并检查之前等待更长时间。 这段额外的时间过后,泄漏可能会消失。
Android Studio 的内存分析器显示清理瞬态泄漏的效果
4. 经常测试,及早修复
我们希望通过本文,您将能够追踪并解决您自己的应用程序中的内存泄漏问题! 与许多错误和其他问题一样,最好经常测试并在不良模式在代码库中扎根之前及早修复它们。
作为开发人员,请务必记住,虽然内存泄漏并不总是影响应用程序性能,但使用低端机型和内存较少的手机的用户会欣赏您为他们所做的工作。
原文链接:
你也在“看”吗?