在片段中保留对视图的引用会导致内存泄漏? [英] Keeping a reference to a View in a Fragment causes memory leaks?
问题描述
有人告诉我以下内容,但我有些困惑.
请,您可以确认或提出异议吗?
(片段不是通过 setRetainInstance()
目前,通常的做法是像这样在Fragments中初始化视图:
private lateinit var myTextView:TextView有趣的onViewCreated(view:View,bundle:Bundle){...myTextView = view.findViewById(R.id.myTextViewId)...}
然后我们永远不会使该属性无效.尽管这是一种常见的做法,但它会导致内存泄漏.
背景:
比方说, FragmentA
作为实例字段引用了其 View
的childView.由片段管理器使用特定的FragmentTransaction执行从片段A到B的导航.根据交易的类型,管理中心可能只想杀死 View
,但仍然保留 FragmentA
的实例(请参阅下面的生命周期部分,其中说片段返回到后堆栈的布局").当用户从 FragmentB
导航回 FragmentA
时, FragmentA
的先前实例将显示在最前面,但是会出现一个新的 View
将被创建.
问题是,如果我们在lateinit属性中将实例保留到我们的视图中,并且从不清除对它的引用,则该视图无法完全销毁,从而导致内存泄漏.
请问,对此事有官方答复吗?
关于此事的官方答案是
Memory Profiler是Android Profiler中的一个组件,可帮助您会识别出可能导致结结的内存泄漏和内存搅动,冻结,甚至应用程序崩溃.
在
此报告基于以下操作在上述应用程序上执行(仅考虑我创建的视图,即ConstraintLayout,Framelayout,Button和TextView)
- 打开应用程序:可见活动
- 在活动"中按下了按钮:
FragmentA
可见 - 按下FragmentA中的按钮:
FragmentB
可见,FragmentA
onDestroyView()
- 在活动"中按下了按钮:第二个实例
FragmentA
可见,并且FragmentB
onDestroyView()
.(与上一个示例中的步骤2相同,除了FragmentB
充当A,FragmentA
的第二个实例充当B) - 在第二个实例FragmentA中按下按钮:可见
FragmentB
的第二个实例和FragmentA
的第二个实例onDestroyView()
. - 按下后退按钮:可见
FragmentA
的第二个实例和FragmentB
onDetach()
的第二个实例 - 按下后退按钮:第一个实例
FragmentB
可见,第二个实例FragmentA
onDetach()
- 按下后退按钮:可见
FragmentA
的第一个实例和FragmentB
onDetach()
的第一个实例>li> - 按下后退按钮:
FragmentA
的第一个实例onDetach()
- 按下后退按钮:,它关闭了应用程序.
观察
如果您查看报告,则可以请参阅第1步中的每个视图,直到关闭该应用程序为止.在第2步中,分配FragmentA的View(即FrameLayout)及其子项,Button,并在第3步中将其清除,这是预期的.在第3步中,分配了FragmentB的View,即FrameLayout及其子TextView,但在第4步中未清除它,因此导致内存泄漏,但在再次创建View并分配新创建的View时在第7步中将其清除了.另一方面,步骤5中创建的视图只是在步骤6中被清除,不会导致内存泄漏,因为片段已分离,并且它们并没有阻止片段被清除.
结论
我们观察到,将视图保存在片段中的泄漏一直持续到用户返回片段为止.当片段被带回时,即调用onCreateView(),泄漏得以恢复.另一方面,当片段在顶部且只能返回时,不会发生泄漏.基于此,我们可以得出以下结论,
- 当片段中没有远期交易时,将视图保存为强引用没有错,因为它们将在
onDetach()
中清除 - 如果有远期交易,我们可以存储弱引用视图,以便在
onDestroyView()
中清除它们
PS 如果您不了解堆转储,请观看此链接提供了有关内存泄漏的有价值的信息.
我希望我的回答能帮助您清除困惑.让我知道您是否仍然感到困惑?
Somebody told me the following, but I am a bit perplexed.
Please, would you be able to confirm or dispute it?
(the Fragment is not retained via setRetainInstance()
At the moment it is a common practice to initialize views in Fragments like this:
private lateinit var myTextView: TextView
fun onViewCreated(view: View, bundle: Bundle) {
...
myTextView = view.findViewById(R.id.myTextViewId)
...
}
And then we never nullify this property. Though this is a common practise, it is causing a memory leak.
Background to this:
Let's say, FragmentA
has a reference to a childView of it's View
, as an instance field.
Navigation from fragment A to B is executed by FragmentManager using a specific FragmentTransaction. Depending on the type of transaction, the Manager might want to kill the View
only but still persist the instance of FragmentA
(see below lifecycle part where it says "The fragment returns to the layout from the back stack"). When user navigates back from FragmentB
to FragmentA
, the previous instance of FragmentA
will be brought to the front, but a new View
will be created.
The issue is that if we keep instance to our view in the lateinit property and never clear the reference to it, the view cannot be fully destroyed, causing memory leak.
Please, is there an official answer on the matter?
An official answer regarding this matter is,
The Memory Profiler is a component in the Android Profiler that helps you identify memory leaks and memory churn that can lead to stutter, freezes, and even app crashes.
In this documentation, android officials teach you how to figure out memory leak by yourself so that they don't have to answer on each and every test cases a user may perform. Besides you can use LeakCanary which does a great job in detecting memory leak.
For your convenience, I performed a heap analysis (a similar but extended version of your use case). Before showing the analysis report I would like to give a step by step basic overview of how the memories will be de/allocated in your case,
On open
FragmentA
: It's content/rootView
and theTextView
will be allocated into the memory.On Navigate to
FragmentB
:onDestroyView()
ofFragmentA
will be called, butFragmentA
'sView
can not be destroyed because theTextView
holds a strong reference to it andFragmentA
holds a strong reference toTextView
.On Navigate back to
FragmentA
fromFragmentB
: The previous allocation of theView
andTextView
will be cleared. At the same time, they will get new allocations as theonCreateView()
is called.On Back press from
FragmentA
: The new allocations will be cleared as well.
Answer to your question:
In step 2, we can see there is a memory leak as the retain memory of View is not freed up what it was supposed to be. On the other hand, from step 3 we can see that the memory is recovered as soon as the user returns back to the Fragment
. So we can figure out that this kind of memory leak persist till the FragmentManager
brings the Fragment
back.
Example / Statical Analysis
To test your case, I have created an application. My application has an Activity
with a Button
and a FrameLayour
which is the container for fragments. Pressing the Button
will replace the container with FragmentA
. FragmentA
contains a Button
, pressing that will replace the container with FragmentB
. FragmentB
has a TextView
which is stored in the fragment as an instance field.
This report is based on the following operation performed on the above application (Only the Views that I created i.e. ConstraintLayout, Framelayout, Button and TextView are taken in consideration),
- Opened the app: Activity visible
- Pressed the Button in the Activity:
FragmentA
visible - Pressed the Button in FragmentA:
FragmentB
visible andFragmentA
onDestroyView()
- Pressed the Button in the Activity: 2nd instance
FragmentA
visible andFragmentB
onDestroyView()
. (This is the same as step 2 in the previous example except,FragmentB
acts as A and the 2nd instance ofFragmentA
acts as B) - Pressed the Button in the 2nd instance FragmentA: 2nd instance of
FragmentB
visible and 2nd instance ofFragmentA
onDestroyView()
. - Pressed Back Button: 2nd instance of
FragmentA
visible and 2nd instance ofFragmentB
onDetach()
- Pressed Back Button: 1st instance of
FragmentB
visible and 2nd instance ofFragmentA
onDetach()
- Pressed Back Button: 1st instance of
FragmentA
visible and 1st instance ofFragmentB
onDetach()
- Pressed Back Button: 1st instance of
FragmentA
onDetach()
- Pressed Back Button: which closed the application.
Observation
If you look into the report you can see, in step 1, each and every view lived until the app is closed. In step 2, the View of FragmentA i.e. FrameLayout and it's child, Button are allocated and got both cleared in step 3 which is expected. In step 3, the View of FragmentB i.e. FrameLayout and its child TextView is allocated but did not get cleared in step 4 hence, caused memory leak but cleared in step 7 when it's View is created again and allocated newly created View. On the other hand, the Views that are created in step 5 just got cleared in step 6 causing no memory leak, because the fragment was detached and they didn't prevent the fragment from being cleared up.
Conclusion
We observed that the leak from saving views in fragment lasts until the user returns back to the fragment. When the fragment is brought back i.e. onCreateView() is called, the leak is recovered. On the other hand, no leak happens when the fragment is on top and can only go back. Based on it, we can make the following conclusion,
- When there is no forward transaction from a fragment, there is nothing wrong with saving views as a strong reference as they will be cleared in
onDetach()
- If there is a forward transaction we can store weak references of views so that they are cleared in
onDestroyView()
P.S. If you don't understand heap dump, please watch Google I/O 2011: Memory management for Android Apps. Also, this link provides valuable information about memory leak.
I hope my answer helped you clear your confusion. Let me know if you still have confusion?
这篇关于在片段中保留对视图的引用会导致内存泄漏?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!