Performance Considerations for Memory Leaks: An Android Cookbook

A quick reference guide loosely based on “Programming Kotlin with Android: Programming Kotlin with Android”

Amanda Hinchman
Google Developer Experts

--

A blank recipe book open, surrounded by wooden spoons holding cheeses, eggs, herbs, garlic, and other ingredients.

Out in the wild, Android faces real-life challenges that affect performance and battery life. For example, not everyone has unlimited data in their mobile plans, or reliable connectivity. Android apps must compete with one another for limited resources.

Performance considerations allow you to examine concerns that may impact your application’s ability to scale. If you can use any of these strategies as “low-hanging fruit” in your code base, it’s well worth going for easy improvements that can greatly reduce overall memory consumption.

A memory leak occurs when:

  • When the heap holds to allocated memory longer than necessary
  • When an object is allocated in memory but is unreachable for the running program

Memory leaks can be found in application code, dependencies, the Android OS — even the JVM. It’s difficult to provide an exhaustive list of possible memory leaks in Android, but showing a broad range can help us better characterize what memory leaks can look like (and quick improvements/fixes to go with them).

Android components & clashing lifecycles

Android is made up of a library of running components. Android app components each have a lifecycle of their own, which is created, managed, and destroyed by the Android framework, not by application code. This is what makes the Android OS unique to how other OS’s work, and what makes memory leaks in Android so….. special. Here’s the quick score card for related examples and what to do with them:

1. Statically-saved Android UI components → remove
2. Android UI interaction/DI within non-Android classes → remove
3. Playing with Bitmaps → reduce, reuse, recycle!

1. Statically-saved Android UI components → remove

Statically saved references are stored permanently in the JVM, meaning that it is never garbage-collected throughout the lifetime of the application. Avoid accidentally saving Android Activity/View using a static keyword in Java or within a companion object in Kotlin.

A View in Android has an implicit reference to the Activity it is associated with via getContext() and is able to reference its own children as well. If any objects happens to hold a reference to an Activity, its Context, or any of its Views when it is destroyed, then the Activity leaks along with its entire View tree. Big leaks.

So remove those static references to UI components!

2. Android UI interaction/DI from non-Android classes → remove

Developers will commonly inject Android components within non-component classes. However, the problem with doing such is that this also requires developers to attempt to manage an Android component lifecycle themselves. It’s not that it’s impossible to do so, it’s more that it’s crazy difficult to account for all possible cases as to View/Fragment/Activity lifecycle. This includes Android Jetpack components like Navigation and Composable views as well!

Keep Android components and interaction with only within Android component classes: this way, having to handle lifecycle considerations is not part of your work.

One common mistake — never inject an an Activity/Fragment (or its Context) directly into your presenter class!

Using an interface to tie to the related Activity/Fragment is how we can send a callback to update the UI while keeping the responsibility of UI interaction within the Activity/Fragment.

Business logic is intended to hold state even after an Activity itself dies… but just the state!

3. Playing with Bitmaps → Reduce, reuse, and recycle!

Bitmaps can easily exhaust an app’s memory budget. For example, the camera on a Pixel phone takes photos of up to 4048 x 3036 pixels (12 megapixels). If the bitmap configuration used is ARGB_8888, a single photo uncompressing and loading from memory takes ~48MB memory (4048 x 3036 x 4 bytes). Such a large memory demand can immediately use up all the memory available to the app, and that’s no good for your users!

That’s why it’s important to reduce, reuse, and recycle bitmaps whenever you can. Scale down your image quality if you’re using it as a tiny profile AVI, cache and reuse already drawn bitmaps whenever you can.

Bitmaps are not only a large source of possible consumption, but they’re also a source of possible memory leaks if you forget to recycle.

Make use of bitmap.recycle() to reclaim memory when a bitmap is no longer being used. If your app is displaying large amounts of bitmap data, it’s likely to run into OutOfMemoryError. The recycle() method helps an app to reclaim memory as soon as possible.

Clashing lifecycles… but with threading :(

Working with threads are already complex as is, and requires an understanding of thread safety rules. A thread has a lifecycle of its own separate from the lifecycle of any Android component: a thread starts, does some work, and when that work is done, the thread dies. If the work doesn’t finish/cancel, then the thread can’t die.

Here’s a quick scorecard of examples of possible sources of concurrent memory leaks and what to do with them:

4. Android UI references in background threads  remove
5. Non-static inner classes within Views/Activities
remove

4. Android UI references in background threads → remove

Suppose a worker thread is created which holds a reference to some Activity in a background thread shortly after the Activity itself is created. In Android memory management, every thread is represented by “regions” in the heap. No one region can access an object within another “region”.

When a user is on an application and rotates the device, the Activity is destroyed. The worker thread created from the Activity will continue to run until its work is complete. Unfortunately, this work never completes thanks to the newly unreachable reference to the previous instance of the Activity.

GC might not be able to dispose of the dead Activity reference and worse: when the thread finishes, it tries to update the reference to the Activity but with nothing to update, leading to an app crash.

So Android UI components are not thread-safe. It’s possible to kill an Activity but this does not necessarily kill a background thread. Because of this volatile complexity, memory leaks are possible as any instances of a component can easily turn into dead weight.

How are we supposed to update the UI from threaded work? The trick here is to force the top-level Activity or Fragments to be the sole system responsible for updating the UI objects.

It is the developer’s responsibility to make sure heavy work is sent to the background thread. If the UI must be updated, then the results are returned to the Main thread before updating the UI.

5. Non-static inner classes within Views/Activities→ remove

An inner class within holds an implicit reference to the enclosing class until the object is destroyed. This is a tricky and common source of memory leaks, especially around threading:

If the instance of TileImageView is destroyed, an implicit reference will be held hostage by whatever running thread has been keeping TileImageLoadedCallback alive.

Marking an inner class with a static keyword can be a solution, given that a static class may not reference the containing class members without an explicit reference and is considered a top-level declaration.

This issue can also be fixed by passing a listener pattern like so:

Now we are passing a listener to trigger the onSuccess call, which is now implemented in its parent class. When onSuccess starts a new thread, it is no longer holding an implicit reference to the parent class (in this case, MapTileActivity).

Did you spot any of these easy changes in your code base? If so, you can fix your own memory leak and check for differences in memory consumption by making an .hprof recording with the Memory Profiler in Android studio! You can also import your .hprof recording to drill down deeper with Eclipse’s Memory Analyzer, or use other open source performance tooling such as Perfetto.

How do I spot other kinds of memory leaks in Android?

You’ll need two things: 1) memory profiling and 2) growing in-depth knowledge of Android component lifecycles and memory management. If you liked this article, you can find more in-depth considerations for Android performance and memory management around concurrency in the newly published Programming Android with Kotlin: Achieving Structured Concurrency with Coroutines.

You can also check out part 2 of this mini series for another quick set of wins with low effort and big payoff!

--

--

Amanda Hinchman
Google Developer Experts

Kotlin GDE and Android engineer. Co-author of O'Reilly's "Programming Android with Kotlin: Achieving Structured Concurrency with Coroutines"