Since Fragments entered our development world, they made some of our job simpler. With fragments, it is now easier to divide the mobile screen into two pieces. Or, another good example from enter/exit animations when navigating another page is much more efficient than working with activities. However, fragments also brought more lifecycle complexities on top of already complex activity lifecycles. Unfortunately, with more lifecycles to keep in mind would mean the more risk for memory leaks to sneak to the production. Therefore, in this article, we will try to cover the 5 most common android memory leaks might occur due to lifecycle complexities.
Table of Contents
What is the Memory Leak?
Before we get into any details of the memory leaks, it is a good practice to understand how they happen and what we should do to avoid them. In Android projects, we have a helper assistant called garbage collector. Garbage collector frequently looks for unused objects to clean up from the memory. With our great assistant, we can have a quite efficient app with very less effort.
Additionally, in android projects, the object/process allocates largest space in the memory is the View itself. So, our great assistant will always look for to clean up any unused View. With that being said, it is easier to imagine that a memory leak can occur if garbage collector cannot clean up the View allocation from the memory. This happens usually if there is another object holds a strong reference to the View that garage collector is trying to clean up.
1. ComposeView Memory Leaks
At first, this kind of memory leak is challenging to spot on. With the help of Leak Canary, it can be easily seen what is wrong. You might be thinking that ComposeView is a View. How can a View leak within another View? Should not it be destroyed when
onDestroy (or in the case of fragments
onDestroyView) is called?
You guessed right. There are some scenarios that ComposeView might leak when it is inside a fragment. Fragments can receive
onDestroyView even though the parent activity is still on
onResume. A couple example into this, usage of BottomNavigationView, or navigating one fragment to another using back stack entry. Both of the scenarios, you might choose to replace the existing fragment, but you might also want to keep previous fragment in the back stack so that the user can navigate back when they click back button.
When that is the case, it is a suggested practice that we as android developers need to encounter that the user is going to be back to the previous fragment. And, the android system is going to create another ComposeView as in its nature. However, compose works different than normal View as we know for the fragments. ComposeView’s content is attached to the activity’s View rather than fragment’s View. Technically speaking, fragments are not a View and they only have a parent as a host.
So, we need to make sure that we clear the ComposeView content before the user navigates back. Before jumping in the code, I would like to show an example log from leak canary.
It might look complex to some of us, but the thing that we need to look for is that what the leak is. As you can see in the above, the LeakCanary point that ComposeView is leaking for some reason. And, if you follow the chain upwards, you will notice that Activity is referenced before the fragment. Which means that, when the user navigates away from the fragment, then ComposeView leaks. This happens because android garbage collector is trying to destroy fragment’s view; however, the view reference is hold by the host activity itself. In this case, the garbage collector is unable to remove the view when the fragment receives
onDestroyView event. And, this causes the ComposeView to be leaked.
Don’t worry! The solution is way simpler than explaining the issue. Jetpack Compose comes with a great solution here,
setViewCompositionStrategy. If you set this strategy within your fragment, ComposeView will clear itself depending on the lifecycle. The code should look like;
As seen above, all we did is to determine the strategy and pass the fragment’s view lifecycle owner instead of activity’s lifecycle owner (which is default strategy for Compose). For anyone who interests more in this topic, you can review this post from android developer.
|DisposeOnDetachedFromWindow||The composition will be disposed when underlying ComposeView is detached from window.|
|DisposeOnDetachedFromWindowOrReleasedFromPool (Default)||Similar to |
|DisposeOnLifecycleDestroyed||The Composition will be disposed when the provided lifecycle is destroyed.|
|DisposeOnViewTreeLifecycleDestroyed||The Composition will be disposed when the |
2. DataBinding/ViewBinding Memory Leaks
One other common memory leak is from binding. DataBinding/ViewBinding is a great tool to avoid calling
findViewById in our fragment or activity. Since it is type safe, we do not need to do type casting anymore. The binding will generate an implementation class that holds the child View types. Furthermore, binding has absolute knowledge that the View is in the hierarchy in compile time, so that we do not even need to do
null check. However, in some cases, you may want to use binding instance in rest of the activity/fragment. It is very common that the fragment/activity might have some
private functions which requires binding instance to be available. Until this point, everything might sound fine, and looks like nothing can leak actually. But, the devil is in the details.
As in the compose view memory leak,
onDestroyView also plays a great role here. When a fragment receives Fragment#onDestroyView event, any references to the binding will cause a memory leak. This might seem very small issue, but it can improve your app performance. It can be imagined as fragment is trying to hold on a view reference that is getting collected by the garbage collector (Which cannot happen, due to the its fragment). A leak canary report may look like below;
As seen above, Leak Canary can even pin point the exact variable name, like in line 37. It is easier to fix than explaining. So, let’s imagine we have some fragment, and it holds binding reference as a class level variable. In order to solve the memory leak here, all we need to do is to set the binding instance back to
null when Fragment#onDestroyView event is received.
3. RecyclerView Adapter Memory Leaks
The next memory can be count as combination of RecyclerView and DataBinding. It is fair to say that most of us either write or see this kind of code below; (setting adapter from data binding itself)
Simply what is going on here, you might be using data binding to set RecyclerView adapter. More than likely, you might have some API calls or DB to store the items in the list. When you try to fetch those items, it will be async process and it will take some time. Therefore, you already need to have the RecyclerView adapter’s instance in your ViewModel so that you can update the list properly when the fetch operation is completed.
Although everything seems fine here, (you guessed) it is not fine for Fragment#onDestroyView event. Here the issue lies on the internal workings in RecyclerView and its adapter. And spoiler alert, the adapter here is the cause of memory leak. First thing to notice is that, we hold the adapter within our ViewModel. So we know for the fact that when the fragment comes foreground again, we will use the same instance of the adapter. And, DataBinding should re-set the adapter when Fragment#onCreateView is received. However, you might ask, how can this adapter even leak? RecyclerView depends on the adapter, not the other way around The adapter does not depend on RecyclerView. Because that is what would cause the memory leak, right?
Yes, that is exactly what is happening here. When you set your RecyclerView adapter, there is a both way of communication is established. And if you are not cleaning that connection, of course, garbage collector cannot clear RecyclerView, so that the memory leak will occur.
As you can above, while RecyclerView depends on the adapter, the adapter also holds a reference to the RecyclerView, so that when you want to update the list, you can do so by simply sending an event from adapter to RecyclerView.
Now we understand that the reference to RecyclerView within the adapter is the cause of the memory leak. So, what is the solution? The solution is also built in the RecyclerView itself. Whenever RecyclerView’s adapter is set, RecyclerView will then clear all the previous communication and establish brand new ones. Then now, we can image to set our adapter to
null to just clean up the connections, and let the view to be destroyed as it should. Later on, when the fragment comes to foreground again, the adapter will be set as in the Fragment#onCreateView event. The logic should look like below;
4. ViewModel Memory Leaks
ViewModels can become really handy; especially, when it comes keeping UI state, holding business logic, and processing async operations. All these are possible because ViewModel does not have any reference to the View or Context, and ViewModels will retain the same instance after any configuration change. This special feature allows developers to start DB connection or API call in the ViewModel without considering any lifecycle changes. However, there is still one lifecycle that we need to consider even though it is way simpler than activity/fragment. It is
onCleared. This function will be triggered after parent View is destroyed and will not be re-created. This callback should be used as clearing process of our IO operation within the ViewModel.
For example; let’s imagine a ViewModel that makes a call to a repository to update the UI. If this call is not cleared when the ViewModel#onCleared event is received, it might cause a memory leak.
As you can see above, the fetching process will continue even though the ViewModel is destroyed because the
fetchUiData was never disposed. The solution to overcome this memory leak is to override
onCleared function and use that to dispose the process.
5. ActivityLifecycleCallback Memory Leaks
The final memory leak on the list is from
ActivityLifecycleCallback. This callback is used to determine which activity is currently on the screen and which lifecycle it is in.
ActivityLifecycleCallback can be really useful when you try to get analytics, or the user navigation flow, etc. However, it is very important that the activities that are passed as a parameter to this callback should not be held as a strong reference since this callback mechanism has no control over the activities lifecycles at all.
Let us look at what callback
ActivityLifecycleCallback offers to use, and when can a memory leak happen.
ActivityLifecycleCallback offers so many callbacks about the current activity on the back stack, and the reference to that activity can be held as in above. However, it is very important to set that
currentActivity back to
null since activity might be killed/destroyed, and we would be still holding a reference to it. Hence, it causes memory leaks.
If you are interested more on this topic, please check the developer.android.com post for more information.
It is very challenging and sometimes overwhelming to create reliable Android application for your customers, friends, maybe even for yourselves. If no-one told you yet, you are doing great. And, thank you very much for reading this post until this point. We as Android developers should get all the powerful tools to help us build better and reliable apps. I would recommend to use Leak Canary especially for memory leaks. It is great tool to identify issues and solve the performance penalties. I personally came to know all those issues on my projects, thanks to Leak Canary.
More posts are coming. Until then, stay tuned, and happy coding …