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.
An official answer regarding this matter is,
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 theFragmentManager
brings theFragment
back.Example / Statical Analysis
To test your case, I have created an application. My application has an
Activity
with aButton
and aFrameLayour
which is the container for fragments. Pressing theButton
will replace the container withFragmentA
.FragmentA
contains aButton
, pressing that will replace the container withFragmentB
.FragmentB
has aTextView
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),
FragmentA
visibleFragmentB
visible andFragmentA
onDestroyView()
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)FragmentB
visible and 2nd instance ofFragmentA
onDestroyView()
.FragmentA
visible and 2nd instance ofFragmentB
onDetach()
FragmentB
visible and 2nd instance ofFragmentA
onDetach()
FragmentA
visible and 1st instance ofFragmentB
onDetach()
FragmentA
onDetach()
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,
onDetach()
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?
I don't think there is a direct official answer, but the answer is in the documentation and it can be easily proven. If you check fragment lifecycle and description of onDestroyView method, it says:
onDestroyView
is called the layout is detached and next time fragment attaches the view is recreated.onCreateView
is called in this case.That basically means if you keep any view references in the fragment instance while the fragment is in back stack, you are preventing these views from being garbage collected.
If you are still not convinced, you can take a look at the source code of AOSP. For example ListFragment and PreferenceFragment both set all views references to null in
onDestroyView()
.In addition it's pretty easy to prove there is a memory leak. Just create a fragment with an image view, which displays a full-size image. Keep a reference in your fragment and navigate to another fragment with a fragment transaction. Then tap
force garbage collection
button and create a heap dump with the memory profiler tool and you'll see your fragment is actually holding image view and the image itself, preventing them from being collected. If you repeat the same steps, but setting the reference to null inonDestroyView()
, you'll see your fragment's retained size is much smaller and noImageView
instance is present in the heap anymore.From a practical point of
View
: The main reason for memory leaks is keepingstatic
fields, in particularstatic Context
, which should be avoided. Within an attachedFragment
, this usually is not even required.static
fields should be called.close()
and set tonull
beforesuper.onDetach()
orsuper.onDestroy()
. One does not even have keep handles to any views, when using data-binding. In Kotlin there are also synthetic accessors for that. Keeping handles to views is not required at all, which renders the question obsolete. This was required before both of these existed. Use lint, ktlint or leakcanary or memory-profiler to find potential memory leaks.Disclaimer:
I will post another answer because I think I couldn't extract the exact use case from the question in the first read. I am waiting for the approval of the edit request I made to this question to know I understood the question properly. I am keeping this answer because I believe there are some useful tips and links that might help someone. On the other hand, in certain cases my answer is right.
ABSOLUTELY NOT since the field is declared
private
, no object from outside of theFragment
i.e.Activity
can access a hard reference of it from theFragment
object. Therefore, it will not prevent theFragment
object from being garbage collected.You might ask, will it cause memory leak when I use this reference in an async callback?
In general, to avoid memory leak, you should watch out the following simple patterns,
This official video, DO NOT LEAK VIEWS (Android Performance Patterns Season 3 ep6) will help you understand it better.
Edit2
Indirect official information:
methods of FragmentTransaction
commitAllowingStateLoss()
andcommitNowAllowingStateLoss()
indicate that theFragment
added to backstack with saved state if no explicitly stated otherwise.method of
Fragment
setRetainInstance(boolean retain)
indicates thatFragment
can save its state between orientation changes and otherActivity
recreations - it means thatFragmentManager
exists relatively independently ofActivity
s life cycle and can storeFragment
state even ifActivity
is destroyed.A small remark in the description of
onDestroyView
Which indicates the exact time of saving
Fragment
state.All these points combined almost explicitly state that there is a state of a
Fragment
view and it is stored in a memory between navigation events.Edit2 end
The first issue is in your question.
You state that the code you provided is a common practice to initialise views in Fragments. Well it is not a common practice at all. It is an old and outdated way Google only uses in its samples and examples. While it is enough for samples it is no good for the production.
One of the current official standards for Kotlin from Google is to init views in fragments or activities via synthetics. Google even use this approach in its modern samples and examples. There is a method called
clearFindViewByIdCache()
that is able to get rid of all strong synthetic references when you need it(most commonly inonDestroyView
).The second standard is to use Android Data Binding via
<layout></layout>
,<data></data>
tags in layout xml files andViewModel
s in code. It is applicable both for Kotlin and Java and pretty easy and straightforward. One of the reasons it was done is to get rid of memory leaks while using the old "standard way", make it easy to retain state on configuration changes and unify approach to modern UI layer implementation. To completely get rid of possible memory leak you will have to nullify bindings inonDestroyView
though. If implemented well! it handles all the stuff out of the box including memory leaks(their absence), retaining view state on config changes, updating UI with relevant data from either network or database viaLivaData
, general communication with the UI, handling Android JetPack features and many more. Along with the rest of JetPack features currently it is a Google recommended way to create Android appsThere is a third semiofficial approach - usage of Butterknife. If implemented well it also is able to handle correct releasing of UI resources to avoid common memory leaks related to UI. The library has
bind()
(inonCreateView
) andunbind()
(inonDestroyView
) methods that handle the stuff you mentioned in your question.The last in this answer(but not in production)) method is to use
WeakReference
,SoftReference
orPhantomReference
- it is a general Java programming technique to avoid memory leaks and to allow GC of objects. It is not very common practice in Android but it is still a good way to handle strong references locks.We can declare auto-clearing properties like this:
var myTextView by autoCleared<TextView>()
…and set their value just like we would for a simple property:
myTextView = view.findViewById(R.id.myTextViewId)
So now regarding whether the code in your question causes memory leaks. Well it most certainly do. It is not even a subject to dispute. It is not stated anywhere officially because it is considered as a common knowledge since it is just how the JWM and Android base classes work.
Edit
Some people in answers claim that there is no leak. Well in the traditional Android understanding - there is no leak - not Activity nor Fragment are leaked - fragment references are alive and placed where they need to be - in fragment managers back stack.
The problem is - the leak still persists. It is not a traditional one thus
LeakCanary
won't find it. But you can find it in debug and profiling. It is still a leak though. Strong references to the views inside a fragment retain during the back stack transaction thus - they store their objects. While ordinary text views or buttons are not that heavy for the heap - the ones that store images - are quite the opposite - they can fill the heap pretty fast. It happens because Android wants to save the most of the fragments view state to restore it as fast as it possible - so the user won't see the blank screen. There may be also an issue when two layouts of the same fragment are present in the view hierarchy and the references refer to the old layout that is quite below and currently invisible. It was my bugs since I handled navigation and storing states in a bad and a wrong manner, but it shows that the old view may be present in the heap.Before the Android Jet Pack era this leak was one to ignore, since there were no that extensive fragments usage and navigation between them. So the heap could handle the resources. But now with single Activity approach this may become one of the main reasons of
OutOfMemoryError
using content heavy fragments without clearing resources inonDestroyView()
.Hope it clarifies some corners.
Edit end
Hope it helps.
There's a Fragment lifecycle method called
onDestroyView
which you should override to release any reference to the views.Generally you should only use
lateinit var
view references if yourFragment
is permanently added to theActivity
and it will not be removed.Kotlin View Binding extensions already solve this problem by automatically clearing view cache inside
onDestroyView
.