5 tips to avoid Memory Leaks in Android Apps
A few ways to be a better Android dev with active memory management
Introduction
As Android developers, we’re fortunate to work with Kotlin, a language that handles memory management for us thanks to Java legacy. However, this doesn’t completely protect us from memory-related issues. Memory leaks, often leading to OutOfMemory (OOM) exceptions, can still impact our Android apps.
This article will demystify memory leaks, exploring how they occur and good practice to avoid them.
What is a memory leak
A memory leak occurs when an object is no longer needed but is still held in memory by another object, preventing the garbage collector from reclaiming its space.
As an analogy, imagine a water faucet that’s constantly dripping; over time, a puddle forms. Similarly, a memory leak is a constant drip of unused objects accumulating in memory and eventually leading to OutOfMemory (OOM) exceptions. In addition, high memory usage forces the Garbage Collector to trigger more often, this process is CPU intensive and can slow the app.
What is Garbage collection
Garbage collection is an automatic process that identifies and reclaims memory occupied by unused objects, simplifying memory management for developers and preventing memory leaks. While efficient, it can’t handle everything. When objects are unintentionally held onto, the garbage collector is unable to free up their memory.
Common Causes of Memory Leaks on Android
Several common culprits contribute to memory leaks in Android apps.
First, Inner class and anonymous object, classes can create hidden references to the outer class, preventing garbage collection.
In the snippet below, post
and setOnClickListener
implementations hold an implicite reference to MainActivity. On configuration change (screen rotation for example) the activity won’t be released and will leak.
// Anonymous object example
class MainActivity : ComponentActivity() {
fun myLeak() {
// Anonymous function call with reference to MainActivity
val handler: Handler = Handler()
// post actually creates a Runnable anonymous object
handler.post {
Thread.sleep(60_000)
}
// Another example of anonymous object View.OnClickListener
view?.setOnClickListener {
// do something
}
}
}
There are other instances with inner class that have an implicit reference to the upper class. Below, each Bee object hold a reference to its factory creator object. To avoid reference to the factory, we should move Bee
outside BeeFactory
.
class BeeFactory { // upper class
val myField = "Hello"
inner class Bee(val data = "World")
fun createBee(): Bee {
return Bee()
}
}
// Factory usage leading to leak
fun myFun(){
// 1. anonymous factories
// create the factory on the go then create a Bee
// for each Bee() object, a BeeFactory object will live in memory
val bObject1 = BeeFactory().createBee()
val bObject2 = BeeFactory().createBee()
// 2. one factory
// a better way is to create the factory once and then reuse
// only one BeeFactory object will live in memory
val bFactory = BeeFactory()
val bObject1 = bFactory.createBee()
val bObject2 = bFactory.createBee()
}
// ideally we should move Bee outside the factory
class Bee(val data = "world")
Note: static inner class do not hold an implicite reference and are also a solution to memory leaks.
Second, large objects, especially bitmaps, consume significant memory. Improper handling can lead to leaks. Always resize bitmaps (with createScaledBitmap
) to fit your needs and recycle them when no longer used.
Note that Glide and Coil do that for you.
// Store bitmap in memory
val bitmapList: HashMap<String, Bitmap> = HashMap()
list.put("item1", createBitmap(200, 200))
// Bitmap usage in ImageView
val view = ImageView(context)
view.setImageBitmap(bitmapList.get("item1"))
// once the view is destroyed, the bitmap won't be freed
// because a reference still exists in bitmapList
// we must call bitmapList.clear() so there are no reference to the Bitmap to free memory
Activities or fragments can hold onto unnecessary context, causing leaks. For instance, registering a broadcast receiver without unregistering it properly.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
registerReceiver(myBroadcastReceiver,
IntentFilter("my.receiver.intent.filter")
)
}
override fun onDestroy() {
// memory leak happen if unregisterReceiver is not called
unregisterReceiver(myBroadcastReceiver)
super.onDestroy()
}
Third, static fields can prevent object garbage collection. Use them cautiously and avoid holding long-lived references to objects in static fields. You need to manage the memory manually.
In the example below any object added to list
will stay in memory for the life of the program.
internal class MemoryLeakClass {
companion object {
val list: ArrayList<Bitmap> = ArrayList<Bitmap>(100)
}
// Make sure you call clear, or remove element from the list regularly.
// else they will live here for the entire life of the app.
}
Fourth, cursors are used to walkthrough databases and other data structures (like Room database and MediaStore). If not closed properly, they can cause leaks. Always call close()
on cursors after use.
Android libter will warn you in logcat:
System [W] A resource failed to call AbstractCursor.close.
val cursor = context.getContentResolver()
.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
cursor.close()
// ..
// Not calling cursor.close() will cause memory leak
Fifth, Activity or Fragment used for their context but not handling Lifecycle.
When an Activity or Fragment is used for their context, we must release the context on lifecycle event onDestroy()
, it can occure on configuration changes.
// singleton holding a context is a bad idea
class ResourcesUtil private constructor() {
var context: Context? = null
fun destroyInstance() {
instance = null
}
companion object {
var instance: ResourcesUtil? = null
get() {
if (instance == null)
instance = ResourcesUtil()
return instance!!
}
}
}
class MyActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ResourcesUtil.setContext(this@MyActivity)
}
@Override
fun onDestroy() {
// must destroy the instance so underlying context can be freed
ResourcesUtil.destroyInstance()
super.onDestroy()
}
}
Android Studio (and linter) will warn you if you try to use context in a static setup
How to de-reference objects
De-referencing objects, allow the garbage collector to free the memory at the next cleaning iteration. There are a few ways an object can be de-referenced.
Nulling a reference, by assigning null
to the variable.
var myVar = MyData();
myVar = null
Assigning a different object/reference to a reference will free the previously assigned object.
var myVar1 = MyData(123)
var myVar2 = MyData(456)
myVar1 = myVar2
// now the memory can be freed from MyData(123) footprint
Anonymous object, will be garbage collected once they are not used (so make sure they have a short life).
fun shortLifeFun() {
MyData(123)
}
// MyData(123) memory will be freed
// when the function finishes
Crash reports don’t report leaks (properly)
Logcat or Crashlytics might not correctly report OOMs. When memory is low because of memory leak accumulation, an OOM exception can be thrown from anywhere in the app code (the next time some memory is allocated, but fall short), which means that every OOM may have a different stacktrace. Therefore, instead of one crash entry with a multiple crashes, OOMs may get reported as multiple distinct crashes and hide in the root cause of occurring crashes.
Conclusion
Today we learned what a memory leak is and how it can occur. We also learned how to manually free memory with dereferencing, so the memory can be reclaim by the Garbage Collector.
In the next article we will learn how to use WeakReference to help build robust Android apps.
If you read until here, thank you 🙏🏼, please clap 👏🏼 and give some feedback.
This article is sponsored by Android Developer News, app is available on the playstore.