在 Fragment 中保留对视图的引用会导致内存泄漏?

Ant*_*ams 14 android android-fragments

有人告诉我以下,但我有点困惑。

请问,您能确认或提出异议吗?

(该片段不通过 setRetainInstance()


目前,在 Fragment 中初始化视图是一种常见的做法,如下所示:

private lateinit var myTextView: TextView

fun onViewCreated(view: View, bundle: Bundle) {

     ...

     myTextView = view.findViewById(R.id.myTextViewId)

     ...

}
Run Code Online (Sandbox Code Playgroud)

然后我们永远不会取消这个属性。虽然这是一种常见的做法,但它会导致内存泄漏。

背景:

比方说,FragmentA有一个对它的 childView 的引用View,作为一个实例字段。FragmentManager 使用特定的 FragmentTransaction 执行从片段 A 到 B 的导航。根据事务的类型,管理器可能想要杀死View唯一但仍然保留的实例FragmentA(请参阅下面的生命周期部分,其中说“片段从后堆栈返回到布局”)。当用户从FragmentBto返回时FragmentA,前一个实例 的FragmentA将被带到前面,但View会创建一个新实例。

问题是,如果我们在 lateinit 属性中保留视图的实例并且从不清除对它的引用,则视图无法完全销毁,从而导致内存泄漏。

Roa*_*aim 21

请问官方有回复吗?

关于这个问题的官方回答是,

Memory Profiler 是 Android Profiler 中的一个组件,可帮助您识别可能导致卡顿、冻结甚至应用崩溃的内存泄漏和内存流失。

这个文档中,android官方教你如何自己找出内存泄漏,这样他们就不必回答用户可能执行的每一个测试用例。此外,您可以使用LeakCanary,它在检测内存泄漏方面做得很好。

为方便起见,我进行了堆分析(您用例的类似但扩展版本)。在显示分析报告之前,我想逐步介绍一下在您的情况下如何解除/分配内存的基本概述,

  1. 打开时FragmentA它是 content/rootView并且TextView将被分配到内存中。

  2. 在导航到FragmentB onDestroyView()FragmentA将被调用,但FragmentAView不能被破坏,因为 TextView持有的强引用,并FragmentA 拥有强大的参考TextView

  3. On Navigate back to FragmentAfrom FragmentB:之前的分配ViewTextView将被清除。同时,它们将在onCreateView()调用时获得新的分配。

  4. On Back press from FragmentA新的分配也将被清除。

回答你的问题:

在第 2 步中,我们可以看到存在内存泄漏,因为 View 的保留内存没有释放它应该释放的内存。另一方面,从第 3 步我们可以看到,一旦用户返回到Fragment. 因此,我们可以计算出,这种内存泄漏持续,直到FragmentManager带来了Fragment回来。

示例/静态分析

为了测试您的案例,我创建了一个应用程序。我的应用程序有一个Activity带 aButton和 aFrameLayour的片段,它是片段的容器。按Button将用 替换容器FragmentAFragmentA包含Button,按下 将用 替换容器FragmentBFragmentB有一个TextView作为实例字段存储在片段中。

在此处输入图片说明

此报告基于对上述应用程序执行的以下操作(仅考虑我创建的视图,即 ConstraintLayout、Framelayout、Button 和 TextView)

  1. 打开应用程序:活动可见
  2. 在活动中按下按钮: FragmentA可见
  3. 按下 FragmentA 中的按钮: FragmentB可见和FragmentA onDestroyView()
  4. 按下活动中的按钮:第二个实例FragmentA可见和FragmentB onDestroyView(). (这与前面示例中的第 2 步相同,只是FragmentB充当 A 并且FragmentA充当 B的第二个实例)
  5. 按下按钮在第二个实例FragmentA:的第二个实例FragmentB可见和第二个实例FragmentA onDestroyView()
  6. 按后退按钮:的第二个实例FragmentA的可见光和第二个实例FragmentB onDetach()
  7. 按下后退按钮:FragmentB可见的第一个实例和第二个实例FragmentA onDetach()
  8. 按后退按钮:第1位实例FragmentA的可见和第一实例FragmentB onDetach()
  9. 按下后退按钮:第一个实例FragmentA onDetach()
  10. 按下后退按钮:关闭应用程序。

观察

如果您查看报告,您可以看到,在第 1 步中,每个视图都一直存在,直到应用程序关闭。在第 2 步中,FragmentA 的视图即 FrameLayout 及其子元素 Button 被分配并在第 3 步中被清除,这是预期的。在第 3 步中,FragmentB 的视图即 FrameLayout 及其子 TextView 已分配,但在第 4 步中没有被清除,因此导致内存泄漏,但在再次创建它的视图并分配新创建的视图时在第 7 步中被清除。另一方面,在步骤 5 中创建的视图在步骤 6 中刚刚被清除,不会导致内存泄漏,因为片段被分离并且它们没有阻止片段被清除。

结论

我们观察到在片段中保存视图的泄漏持续到用户返回片段。当片段被带回即 onCreateView() 被调用时,泄漏被恢复。另一方面,当片段在顶部时不会发生泄漏并且只能返回。基于此,我们可以得出以下结论,

  • 当没有来自片段的前向交易时,将视图保存为强引用并没有错,因为它们将在 onDetach()
  • 如果有远期交易,我们可以存储视图的弱引用,以便它们在 onDestroyView()

PS如果您不了解堆转储,请观看Google I/O 2011:Android 应用程序的内存管理。此外,此链接提供了有关内存泄漏的宝贵信息。

我希望我的回答能帮助你清除你的困惑。让我知道你是否还有困惑?


Roa*_*aim 6

免责声明:

我将发布另一个答案,因为我认为我无法从第一次阅读的问题中提取确切的用例。我正在等待我对此问题提出的编辑请求的批准,以确保我正确理解了该问题。我保留这个答案是因为我相信有一些有用的提示和链接可能会对某人有所帮助。另一方面,在某些情况下我的答案是正确的。

在 Fragment 中保留对 View 的引用会导致内存泄漏吗?

绝对不能,因为声明了该字段,来自ieprivate外部的任何对象都不能从该对象访问它的硬引用。因此,它不会阻止对象被垃圾收集。FragmentActivityFragmentFragment

You might ask, will it cause memory leak when I use this reference in an async callback?

My answer would be, yes it will cause memory leak for keeping reference inside the async callback but not due to keeping its reference in the Fragment. However, this memory leak will also happen even if you don't store the View reference in the Fragment.

In general, to avoid memory leak, you should watch out the following simple patterns,

  1. Never reference views inside Async callbacks
  2. Never reference views from static objects
  3. Avoid storing views in a collection that stores values as hard references

This official video, DO NOT LEAK VIEWS (Android Performance Patterns Season 3 ep6) will help you understand it better.


Pav*_*sha 5

编辑2

间接官方信息:

这表示保存Fragment状态的确切时间。

所有这些点结合起来几乎明确地表明存在Fragment视图状态,并且它存储在导航事件之间的内存中。

编辑2结束

第一个问题是在你的问题中。

您声明您提供的代码是在 Fragments 中初始化视图的常见做法。嗯,这根本不是一种常见的做法。这是 Google 仅在其示例和示例中使用的一种陈旧过时的方式。虽然对于样品来说已经足够了,但对生产来说却没有好处。

  • Google 目前对 Kotlin 的官方标准之一是通过合成在片段或活动中初始化视图。谷歌甚至在其现代示例和示例中使用这种方法。有一种方法clearFindViewByIdCache()可以在您需要时删除所有强合成引用(最常见于onDestroyView)。

  • 第二个标准是通过<layout></layout>,<data></data>布局xml文件中的标签和ViewModel代码中的 s使用Android数据绑定。它适用于 Kotlin 和 Java,而且非常简单明了。这样做的原因之一是在使用旧的“标准方式”时消除内存泄漏,使在配置更改时保持状态变得容易,并统一现代 UI 层实现的方法。要完全摆脱可能的内存泄漏,您将不得不取消绑定onDestroyView。如果执行得好!它处理所有开箱即用的东西,包括内存泄漏(它们不存在),在配置更改时保留视图状态,使用来自网络或数据库的相关数据更新 UI,LivaData与 UI 的一般通信,处理Android JetPack功能等等。与当前 JetPack 的其他功能一起,它是Google 推荐的创建 Android 应用程序的方式

  • 还有第三种半官方方法——使用Butterknife。如果实现得好,它还能够处理 UI 资源的正确释放,以避免与 UI 相关的常见内存泄漏。该库具有bind()(in onCreateView) 和unbind()(in onDestroyView) 方法来处理您在问题中提到的内容。

  • 这个答案中的最后一个(但不是在生产中))方法是使用WeakReference, SoftReferenceorPhantomReference - 它是一种通用的 Java 编程技术,可以避免内存泄漏并允许对象的 GC。这在 Android 中并不是很常见的做法,但它仍然是处理强引用锁的好方法。

奖金!)不用担心onDestroyView您可以使用委托技术和AutoClearedValueKotlin。

我们可以像这样声明自动清除属性:

var myTextView by autoCleared<TextView>()

...并设置它们的值,就像我们为一个简单的属性设置的一样:

myTextView = view.findViewById(R.id.myTextViewId)

所以现在关于您问题中的代码是否会导致内存泄漏。那么它肯定会。它甚至不是一个有争议的话题。它没有在任何地方正式说明,因为它被认为是一个常识,因为它就是 JWM 和 Android 基类的工作方式。

编辑

有些人在回答中声称没有泄漏。好吧,在传统的 Android 理解中 - 没有泄漏 - Activity 和 Fragment 都没有泄漏 - 片段引用是活着的并放置在它们需要的地方 - 在片段管理器的后堆栈中。

问题是 - 泄漏仍然存在。因此它不是传统的LeakCanary不会找到它。但是您可以在调试和分析中找到它。尽管如此,它仍然是一个泄漏。因此,在返回堆栈事务期间保留对片段内视图的强引用 - 它们存储它们的对象。虽然普通的文本视图或按钮对于堆来说并没有那么重 - 那些存储图像的 - 恰恰相反 - 它们可以非常快地填充堆。发生这种情况是因为 Android 想要保存大部分片段视图状态以尽快恢复它 - 因此用户不会看到空白屏幕。当视图层次结构中存在相同片段的两个布局并且引用引用非常低且当前不可见的旧布局时,也可能存在问题。这是我的错误,因为我以错误和错误的方式处理导航和存储状态,

在 Android Jet Pack 时代之前,这种泄漏是可以忽略的,因为它们之间没有那么广泛的片段使用和导航。所以堆可以处理资源。但是现在使用单个 Activity 方法,这可能成为OutOfMemoryError使用内容繁重的片段而不清除onDestroyView().

希望它能澄清一些角落。

编辑结束

希望能帮助到你。


Paw*_*wel 1

有一个 Fragment 生命周期方法,onDestroyView您应该重写该方法以释放对视图的任何引用。

lateinit var一般来说,如果您的视图引用Fragment被永久添加到Activity并且不会被删除,您应该只使用视图引用。

Kotlin View Binding 扩展已经通过自动清除内部视图缓存解决了这个问题onDestroyView