Why does clearFindViewByIdCache() have to be called in onDestroyView()? - android

I'm trying to understand Kotlin Android extensions and this article says says:
When asked for a view, it will try to find it in the cache. If it’s not there, it will find it and add it to the cache. Pretty simple indeed.
Besides, it adds a function to clear the cache: clearFindViewByIdCache. You can use it for instance if you have to rebuild the view, as the old views won't be valid anymore.
Why won't the old views be valid anymore once you rebuild the view after onDestroy()? The view references are still going to be there once you rebuild the view, for example in OnActivityCreated().

The important note here is that Fragments usually have a longer lifecycle then their views. Let's consider the following flow:
You created Fragment A and placed it into a container
Fragment A view got created (this onCreateView + onViewCreated)
You created Fragment B and replace Fragment A with the new fragment in the container + keep back stack
Fragment A views will be destroyed at this moment, but the fragment will be kept inside of FragmentManager.
You back press and Fragment B got fully destroyed because you logically leaving that. At this moment system replaces it back with Fragment A and it creates a new view again.
So here are a few important notes:
At step #4 if you keep any links to subviews, then your memory just leaks because all of them already got detached out of your view hierarchy and can't be used anymore. Basically, you still hold all those views in memory, even though they are not in use anymore. Usually, ppl don't notice that and all views will get rebound again with new onViewCreated (and findViewById), but kotlin extensions keep them in a cache and basically do not execute this findViewById again, so you can get old (read as dead) views that are not those views that just got created.
If you use ButterKnife at the moment for instance, they have description about that as well in Binding Reset section. (http://jakewharton.github.io/butterknife/#reset).
Last important note is that Kotlin extensions do this job for you, so you don't really need to call it inside of onDestroyView, you just use it and it works (as magic...)

Related

Keeping a reference to a View in a Fragment causes memory leaks?

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/root View and the TextView will be allocated into the memory.
On Navigate to FragmentB: onDestroyView() of FragmentA will be
called, but FragmentA's View can not be destroyed because the
TextView holds a strong reference to it and FragmentA
holds a strong reference to TextView.
On Navigate back to FragmentA from FragmentB: The previous
allocation of the View and TextView will be cleared. At the same time, they will get new allocations as the onCreateView() 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 and FragmentA onDestroyView()
Pressed the Button in the Activity: 2nd instance FragmentA visible and FragmentB onDestroyView(). (This is the same as step 2 in the previous example except, FragmentB acts as A and the 2nd instance of FragmentA acts as B)
Pressed the Button in the 2nd instance FragmentA: 2nd instance of FragmentB visible and 2nd instance of FragmentA onDestroyView().
Pressed Back Button: 2nd instance of FragmentA visible and 2nd instance of FragmentB onDetach()
Pressed Back Button: 1st instance of FragmentB visible and 2nd instance of FragmentA onDetach()
Pressed Back Button: 1st instance of FragmentAvisible and 1st instance of FragmentB 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?
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.
Keeping a reference to a View in a Fragment causes memory leaks?
ABSOLUTELY NOT since the field is declared private, no object from outside of the Fragment i.e. Activity can access a hard reference of it from the Fragment object. Therefore, it will not prevent the Fragment object from being garbage collected.
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,
Never reference views inside Async callbacks
Never reference views from static objects
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.
Edit2
Indirect official information:
methods of FragmentTransaction commitAllowingStateLoss() and commitNowAllowingStateLoss() indicate that the Fragment added to backstack with saved state if no explicitly stated otherwise.
method of Fragment setRetainInstance(boolean retain) indicates that Fragment can save its state between orientation changes and other Activity recreations - it means that FragmentManager exists relatively independently of Activitys life cycle and can store Fragment state even if Activity is destroyed.
A small remark in the description of onDestroyView
Internally it is called after the view's state has been saved but before it has been removed from its parent.
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 in onDestroyView).
The second standard is to use Android Data Binding via <layout></layout>, <data></data> tags in layout xml files and ViewModels 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 in onDestroyView 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 via LivaData, 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 apps
There 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()(in onCreateView) and unbind()(in onDestroyView) 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 or PhantomReference - 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.
Bonus!)
Not to worry about onDestroyView you can use delegation technique and AutoClearedValue in Kotlin.
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 in onDestroyView().
Hope it clarifies some corners.
Edit end
Hope it helps.
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:
After onDestroyView is called the layout is detached and next time fragment attaches the view is recreated.
The fragment can actually come back from the back stack and 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 in onDestroyView(), you'll see your fragment's retained size is much smaller and no ImageView instance is present in the heap anymore.
From a practical point of View: The main reason for memory leaks is keeping static fields, in particular static Context, which should be avoided. Within an attached Fragment, this usually is not even required. static fields should be called .close() and set to null before super.onDetach() or super.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.
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 your Fragment is permanently added to the Activity and it will not be removed.
Kotlin View Binding extensions already solve this problem by automatically clearing view cache inside onDestroyView.
If you use view binding, you should implement it like in the docs. Like so,
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
}
This fix the View memory leak with LeakCanary for me.

Understanding of MvxCachingFragmentCompatActivity

I need to understand how to use MvxCachingFragmentCompatActivity. I have asked this question before previous question, but I got a piece of example code, which is helpful but not what I needed. What I need is an understanding of how to use it.
First of all I have one activity and all my views are fragments.
My big assumption here is that using MvxCachingFragmentCompatActivity will enable me to restore my application navigation hierarchy if my activity is torn down and needs to be restored. Can someone confirm if this is correct.
If this is correct how do I use it. For example
Do I need to implement Save and Restore state in the view models? Is
there anything else the developer needs to do?
What does the MvxFragmentAttribute parameter IsCacheableFragment
actually do as regards caching fragments?
What performs the action of recreating my fragment hierarchy when an
activity is restored?
It would be great if there was some documentation around this.
I need to know this as my Activity is being torn down and then restored after I use another Activity for a camera feature. When the Activity restores itself the ViewModel for my fragments are null.Also I am finding Close(this) does not work in my view model. I'm sure I am not doing everything I need to do to make this work, but I need guidance on how it is supposed to be used.
Any help would be appreciated, maybe someone from the MvvmCross team. I'm really stuck here. I would prefer a description of the behaviour rather than point to a sample, but both would be great.
[Updated]
So I built a debug version of the V4 and V7 MvvmCross libraries and set about debugging. As far as I can tell as long as you add the following attributes to your fragment class this should set about caching your fragments.
[MvxFragment(typeof(MainActivityViewModel), Resource.Id.contentFrame, AddToBackStack = true, IsCacheableFragment = true)]
[Register("com.dummynamespace.MyFragment")]
Note the lowercase namespace is important, your class name can be mixed case.
However I am still seeing problems after my activity is destroyed and re-created. In my case I am actually seeing my activity destroyed and recreated more than once in quick succession. One example is that I cannot close the view after the activity destroyed and recreated. This seems to be due to the fact that the code in GetFragmentInfoByTag (MvxCachingFragmentCompatActivity class) is returning the wrong information needed to close the view. The close functionality needs the ContentId from the returned IMvxCachedFragmentInfo, however this is returning it as 0. Also the AddToBackStack property is set to false. Below I have listed what is returned in the fragment info
AddToBackStack = false
CacheFragment = true
CachedFragment = null
ContentId = 0
FragmentType = This is set to the correct fragment type
Tag = This is set to the corresponding view model for the fragment
Before the activity is destroyed and recreated the the fragment info is correct.
I am using MvvmCross 4.2.3. Has anyone else experience this?
Update 02/03/2017
I found out that my activity was being destroyed and recreated not due to memory but due to the camera orientation. We found it only failed when we held the camera in landscape mode.
The issue regarding the ContentId being set to 0 was caused by my app not being able to resolve the IMvxJsonConverter implemenation. This occurs when the MvvmCross Json plugin is not installed. Also you have to add the following to your App.cs file so it can be registered
Mvx.RegisterType<IMvxJsonConverter, MvxJsonConverter>();
If this is not done, then the Try.Resolve fails and the code that uses it is skipped over. Sometimes it is done silently other times it outputs a log. IT would seem to me that this should probably be fatal if you expect your app to survive the activoty being torn down and reconstructed.
Also one the MvvmCross Json plugin is installed you have to implement the Save and Restore state pattern in your view models save-Restore
Update new problem 08/03/2017
I am testing the restore of every view in my app. I am doing this by allowing the orientation to be changed which destroys my MvxCachingFragmentCompatActivity and then re-creates it.
When the activity is destroyed my fragment is also destroyed. At this point I tidy up my view model to ensure it will be free'd up and will not cause a memory leak.
However I have hit a problem where when OnCreate is called. It seems to do two things
Get a view model from the MvxFragmentExtensions OnCreate method by
calling into the view model cache
Then calls RestoreViewModelsFromBundle
The problem is that the call to MvxFragmentExtensions OnCreate (1) calls into the view model cache and returns a view model which has not been Started e.g Start() called on it, but this is used to set the DataContext.
After RestoreViewModelsFromBundle (2) is called the DataContext is not set again event though it has gone through the Constructor->Init->RestoreState->Start set up. So I now have a view model which is not setup properly and so my view does not work.
When I took out my code to tidy the view models, I got a bit further as the cached view model set by (1) now had the correct data. But I am hitting other problems because it is attempting to create a new view model due to the call to RestoreViewModelsFromBundle (2). As a short term fix is there anyway I can force the view model created as part of the restore process to be set as the ViewModel
Can someone from the MvvmCross team please help out with some information as to what is happening here and why?

What happens to a Fragment once it's removed or replaced?

Android documentation doesn't seem to have much on it, other than stating that the fragment is removed once the Transaction is committed. Is the Fragment just gone? Like in a metaphysical sense? Or does it exist somewhere still, able to be called when needed? If so, how would you call it?
As a more pragmatic question, if you have an Activity that contains one fragment View and multiple Fragments that eventually go into that View, is there a way to reference Fragments other than the most recent one on the back stack (for question purposes, assuming they're all placed on there)
When you remove or detach fragment it doesn't removed from the project, it can be attch with another activity.
But once you pop fragment from the stack and destroy, it can't be revoked. Because when you destroy any fragments, a cleanup function is being called which cleans every part of that fragment.

When (& where) are my fragments created

I'm using the viewpagerindicator library (http://viewpagerindicator.com/) to create some sort of wizard for my android app. It works fine and does exactly what I want.
I would like to "extend" the functionality a bit by having "previous"/"next" buttons in my ActionBar - pretty much as in Android's "Done Bar" tutorial - to step through the wizard. Works like a charm, too.
HOWEVER:
I would like to display information about the "next" & "previous" fragment in the ActionBar's buttons. Information I pass to the fragments that live in the ViewPager at the time of their "creation" (actually at the time of their object instantiation - using the classical "newInstance(...)" approach to create the instance of my fragment, store the parameters in a Bundle and extract them in the fragment's "onCreate" method). The same way the template does it, when you create a new fragment for your project.
So, this information is the thing I actually want to display in my wizards button to know what fragment is next and which was last.
The type of this information is not important for my problem. It could be a String or an icon or an int or ... anything else you want.
However, wherever I've tried to access my fragments data, the fragment has not yet been fully initialized (meaning its "onCreate" method has not been called yet).
I've tried it in the host fragment's "onViewCreated" method, because I thought that's where all its subviews should be initialized already (at least their "onCreate" method should have been called, I thought), but it seems that this is handled differently for ViewPager to retain only the number of fragments in memory that was set by setOffscreenPageLimit.
So, what I'm looking for (and probably just missing) is the correct callback method here. One that is called when the ViewPager's next Fragments have been loaded and initialized. If such a callback exists, I could place my little piece of code there to update the text in my "previous"/"next" buttons within the ActionBar.
Any help, comments, ideas are highly appreciated. If needed, I can also try to attach some code sample to better explain my setup, but I think it should be easy enough to understand what my problem is.
Thanks in advance!
P.S.: I also tried to do this by using EventBus to send "onFragmentInitialized" messages from my fragments within in the ViewPager and the hosting fragment. It actually worked, but it does not seems the proper way to do this.
When a Fragment's onCreate Method is called, its already preparing to be displayed, and practically its past the point where its considered a Next or Previous fragment instead its considered current.
A fragment's onCreateViews method is called after committing a transaction in the FragmentManager. which takes less than 1 sec to bring it in front of the user (depending on the device and runtime environment)
But in your case, your data should be initalized outside the Fragment that uses it, and displayed where ever you want by passing the data itself then displaying whatever you want form it.
decouple your data from android objects (Fragment, Activity ...) and you should be able to load, maintain, access it cleanly and without worrying about their callbacks.
The Fragment's arguments can be read and loaded in its onAttach callback rather than onCreate, the Activity will then (after onAttach is complete) get a onAttachFragment callback with the Fragment as a parameter. However, I doubt onAttachFragment will be called when switching between already loaded pages in the view pager.
If not, you could have the fragment notify the activity (through an interface) that it is now active during its onActivityCreated, onViewCreated or similar method.
But it sounds more like the activity should register as a page changed listener to the ViewPager itself, and update its state depending on the page rather than which fragment is active.
As a side note, ViewPagerIndicator is quite old now (hasn't been updated in 3 years), a more modern approach is the SlidingTabs example from Google, which has been built into a library available here: https://github.com/nispok/slidingtabs

setRetainInstance fragment with UI Android

Ok, I created a Fragment with some UI (couple textboxes and stuff) and I used setRetainInstance since Im running an AsyncTask to query a server (request can only be sent once) and I need the result of the AsyncTask. So my question is:
Is it wrong to retain the whole fragment with the UI? I saw couple examples where people use an extra Fragment to use the setRetainInstance but.. is there anything wrong not using that extra one??
If there is an issue with using the setRetainInstance why is that? Couldn't find any info in the documentation regarding this.
Even if you use setRetainInstance(true), your Fragment will still recreate its views when you rotate (you will get a call to onDestroyView and then onCreateView). As long as you don't keep references to views past onDestroyView, there will not be any leaks of the old Activity. The best approach would be to explicitly null the references in onDestroyView, but your code in onCreateView would generally overwrite those references anyway.
There are many examples online (including some official ones) where people use a separate fragment (without a view) to retain data. Assuming what I said above is correct, then this is unnecessary (for the sake of preventing leaks). In many cases, you may end up with cleaner code/architecture if you use a separate fragment whose responsibility is just to handle the data and not worry about the UI.
You can check to see if you are leaking Activity contexts after rotating by using Eclipse MAT.
If you are locking your orientation then you should be fine. Otherwise you can end up with memory leaks if you retain widgets that are associated with a particular activity instance.

Categories

Resources