The ART virtual machine in Android is a managed memory environment. The garbage collector takes care of memory allocation and frees it back to the heap when that piece of memory is no longer being used. Memory leaks occur when objects are no longer being used by the application, but the garbage collector is unable to remove them because they are still being referenced. As a result, these objects are maintained in memory, and consume resources unnecessarily. Eventually, memory leaks will lead to frequent garbage collection and out of memory error.
In this article, we’re going to talk about some of the most common memory leaks in Android and the way to avoid them.
Types of references in Android
Android (or Java) has 4 types of references, from strongest to weakest strength, they are: strong, soft, weak and phantom. Strong and weak references are the most widely used.
Strong references
A strong reference is an ordinary reference, the one created by default every time a new object is created. An object is classified as strongly reachable if it can be reached through a chain of strong references. Strongly reachable objects are not eligible for garbage collection, which is normally what we want.
However, in some cases, strong references can cause us problems because… well… they are strong. Suppose we have a data cache to avoid reloading data when we don't need to. With strong references by default, all the data will be kept in memory, taking up a lot of space. At some point, we need to free up the unused data if the cache keeps growing. We can build some kind of LRU cache or manually keep track of all the objects in the cache, but it is a non-trivial task. The LRU cache also has another problem: we have to set a memory limit for the cache, which is often not easy to determine. There would be nice if we have a simple mechanism to find out which objects are not needed anymore, and should be garbage collected. That is when weak references come into play.
Weak references
A weak reference is a reference not strong enough to keep the object in memory. If the garbage collector finds that an object is weakly reachable (reachable only through weak references), it'll clear the weak references to that object immediately.
With the previous example, we can leverage the garbage collector's ability to determine the reachability of an object to solve the problem. In Android, we have a built-in data structure called WeakHashMap. Elements in a WeakHashMap can be reclaimed by the garbage collector if there are no other strong references to the object's key. Therefore, if a WeakHashMap key becomes garbage, its entry is removed automatically. Of course, WeakHashMap is not always suitable for building a cache. In some scenarios, a cache should remove data depending on the value or a specific expiration policy rather than the key.
Other types of references
There are two more types of references: soft and phantom.
A soft reference is exactly like a weak reference, except that it is less eager to throw away the object to which it refers. In other words, a soft reference still stays in the memory for a while until the memory is absolutely needed, while a weak reference will be collected immediately.
A phantom reference is very different from the rest. We can't get a referent of a phantom reference, and the referent is never accessed directly. Phantom references can be used to determine when an object is removed from the memory, which helps to schedule memory-sensitive tasks or cleanup actions. That said, they seem like a myth with very few use cases.
Memory leaks common patterns
Static fields
Static fields will live as long as the process’s lifetime. If static variables reference heavy data, those data won’t be collected, even though they might no longer be needed. For example, a bitmap cache can fill up your memory very quickly if you don’t have a mechanism to clean it up.
You should also never keep a reference to the activity or context in a static object in android. The activity or context often holds a lot of other objects, and leaking them can cause us trouble. The best way here is just to pass the context as a parameter to the functions every time you call it. But if you really have to keep the context in a static object for some reason, consider using application context instead of view or activity context, and remember to set it to null when it is not needed anymore.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// using application context here, but a better way is not to keep a context as a static field
Utils.context = this.applicationContext
}
override fun onDestroy() {
super.onDestroy()
// clear context reference to avoid memory leaks
Utils.context = null
}
}
object Utils {
// memory leaks can happen here
var context: Context? = null
fun doSomethingWithContext(){
context?.apply {
// doing wonderful things with the context
}
}
}
Long-running task
Activity and Fragment in Android have pretty complicated lifecycles. Doing the work at the wrong time may lead to memory leaks. If the activity starts a background task and that task continues to run while the activity has been destroyed, the activity may not be collected by the garbage collector. If some views are referenced in an asynchronous callback, these views cannot be freed until the task is done. That means the activity, which contains the view, is leaked, as well as all the objects in the activity. Therefore, you should cancel long-running tasks when it is no longer required.
class MainActivity : AppCompatActivity() {
private lateinit var textView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
TaskExecutors.doTask(10, object: (Int) -> Unit {
override fun invoke(result: Int) {
// this may cause memory leaks or even crashes if the task is still running after the activity has been killed
runOnUiThread {
textView.text = result.toString()
}
}
})
}
override fun onDestroy() {
super.onDestroy()
// should cancel running tasks if it is not needed anymore
TaskExecutors.cancelTask()
}
}
object TaskExecutors {
private var job: Job? = null
fun doTask(input: Int, callback: (Int) -> Unit) {
job = GlobalScope.launch {
val result = doHeavyCalculation(input)
callback.invoke(result)
}
}
private suspend fun doHeavyCalculation(input: Int): Int {
delay(10000L)
return input * 10
}
fun cancelTask(){
// find a way to cancel long-running task
}
}
Unclosed Resources
Whenever we create or open a resource, the system allocates memory for these resources. Not closing these resources can block the memory, keeping them from being collected. Some examples of these types include database connections, input streams, or forgetting to unregister the broadcast receiver.
Non-static inner class
Any instance of an inner class contains an implicit reference to its outer class. A memory leak will occur if an instance of an inner class survives longer than its outer class. More specifically, there is a situation where the inner object is alive (via reference) but the references to the containing object have already been removed from all other objects. The inner object is still keeping the containing object alive because it will always have a reference to it.
fun main(args: Array<String>) {
val productContainer = mutableListOf<Factory.Product>()
// need a factory to make product
val factory = Factory()
for (i in 0..10000) {
val newProduct = factory.createProduct()
productContainer.add(newProduct)
}
// create product complete, now we don't need the factory anymore. But...
// ... the factory is still here, because the product has the reference to it.
}
class Factory {
// data to leak
var data = 0
fun createProduct(): Product = Product()
inner class Product { var pId: Int = 0 }
}
The solution for this problem is that, if the inner class doesn't need access to the outer class members, consider changing it into a static class.