← Back to list

LEAK HUNTERS: ADVENTURE IN ANDROID PROFILING

Welcome, leak hunters, to an exciting journey through the mysterious world of memory leaks in Android applications. In this thrilling…

Aytaj Dadashova · 2023-06-29 20:58 · 212 claps · 9.7 min read
#memory-leak #android #android-profiler #leakcanary #memory-profiling
Open on Medium ↗

LEAK HUNTERS: ADVENTURE IN ANDROID PROFILING

cr. tenor.com

cr. tenor.com

Welcome, leak hunters, to an exciting journey through the mysterious world of memory leaks in Android applications. In this thrilling expedition, we will uncover the secrets of memory leaks, learn how to detect and prevent them and explore powerful tools like the Android Profiler and **LeakCanary**. So grab your detective hat🕵🏻 and magnifying glass🔍 as we dive deep into the realm.

WHAT IS A MEMORY LEAK?

The investigation begins with a fundamental question: What exactly is a memory leak? Memory leaks occur when objects are allocated in memory but are not properly released when they are no longer needed. In short, Memory leaks are abandoned objects that are never going to be used but are still accessible.​ These abandoned objects continue to reside in memory, consuming valuable resources and leading to performance degradation and even crashes.

Imagine a criminal leaving behind countless footprints without ever returning to clean up the crime scene — it’s a similar scenario in the world of memory leaks.

BUT WHY GARBAGE COLLECTOR DOESN’T TAKE ACTION?

Yes, exactly, it’s the right question to ask! There is a powerful mechanism called Garbage Collector. Why it doesn’t take action and clear the abandoned objects? Well, let’s review the two conditions that make the Garbage Collector take action. At least one or two of these conditions have to be met for an object to be eligible for Garbage Collector:

The object is no longer accessible by any reference.

All the references that have access to the object are out of scope or have completed their lifecycle.

Java Garbage Collection — Concepts (Performance Engineering)

Java Garbage Collection — Concepts (Performance Engineering)

Then what exactly happens that makes the class leak? In the Memory leak case, there are still remaining strong references to the object preventing it to be eligible for Garbage Collector. As we already said above, Memory leaks are caused by abandoned objects that are no longer going to be used but are still accessible. This can occur due to references held beyond their intended lifespan, such as static variables or unregistered listeners. We’ll understand it better through examples in the next sections.

COMMON SCENARIOS WHERE MEMORY LEAKS CAN OCCUR IN ANDROID

Memory leaks can occur in various scenarios. Here are some common examples:

Leaking Context:

A common memory leak scenario is leaking the Context object. The Context holds references to system resources and other important components of an application. If a long-lived reference to a Context is held when it's no longer needed, it can prevent the associated resources from being garbage collected, leading to a memory leak.

class MainActivity : AppCompatActivity() {
    companion object {
        var context: Context? = null
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

class SecondActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
        MainActivity.context = this
    }
}

In this example, the MainActivity holds a static reference to the SecondActivity’s context. If the SecondActivity is destroyed but the reference to the context is not cleared, it can cause a memory leak.

Leaking Handler:

Another common memory leak scenario involves improper usage of the Handler class. A Handler allows communication between background threads and the main thread by posting messages and runnables to be executed at a later time. Failure to remove the pending message from the Handler can lead to a memory leak because the Handler retains a reference to the Activity .​ If the Activity is destroyed before the delayed message is processed, the Activity won’t be garbage collected, resulting in a memory leak.

class MyActivity : AppCompatActivity() {
    private val handler = Handler()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        handler.postDelayed({ /* Do something */ }, 5000)
    }

    override fun onDestroy() {
        super.onDestroy()

        // Important: Remove any pending messages from the handler
        handler.removeCallbacksAndMessages(null)
    }
}

To prevent this, the pending messages and callbacks should be removed using the removeCallbacksAndMessages(null) method in the onDestroy() method.

SOME COMMON SCENARIOS INCLUDE:

Unreleased Object References:

When objects are no longer needed but references to them are still held, such as in long-lived collections or static variables, it prevents the garbage collector from reclaiming the memory occupied by those objects.

Context Leaks:

Holding references to Activity or Context instances beyond their lifecycle can lead to leaks. For example, registering a BroadcastReceiver or creating a long-running background task without properly cleaning up.

Anonymous Inner Classes:

Using anonymous inner classes can inadvertently cause memory leaks when they hold references to their enclosing class, preventing the garbage collector from collecting the enclosing class and all its resources.

Listener Registration:

Failing to unregister listeners or callbacks when they are no longer needed can result in leaks, as the objects holding those listeners will remain in memory.

STRONG REFERENCE VS WEAK REFERENCE

In Java, objects referenced by strong references are not eligible for garbage collection. However, weak references allow objects to be garbage collected when no strong references to them exist. Understanding the difference between strong and weak references is one of the important points in preventing memory leaks.

class MyActivity : AppCompatActivity() {
    private var myObject: MyObject? = MyObject() // Strong reference

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my)

        val weakRef = WeakReference(myObject) // Weak reference
        // Use weakRef as needed
    }
}

In this example, a WeakReference is used to hold a reference to an object. If there are no other strong references to the object, it can be garbage collected, helping prevent memory leaks.

CLEAN AFTER YOU ARE DONE

To prevent memory leaks, it’s important to clean up references that can cause memory leaks properly. When a variable holds a reference to the Activity or Fragment or ApplicationContext, you have to pay attention if those references are being released properly or not. For example, in an Activity or Fragment, you can set references to null in the onDestroy() method.

class MainActivity : AppCompatActivity() {
    private var biometricPrompt: BiometricPrompt? = null
    get(){
        if (field == null){
            // initialise your field here
        }
        return field
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onDestroy() {
        super.onDestroy()
        biometricPrompt = null
    }
}

By setting the reference to null, you allow the object to be garbage collected when it’s no longer needed, avoiding memory leaks.

ANDROID PROFILER

Stay tuned for this part, where we’ll dive into the powerful Android Profiler and explore how it can assist us in our leak-hunting adventure.

giphy.com

giphy.com

To hunt down memory leaks, we rely on powerful tools like the Android Profiler. The Android Profiler is a part of Android Studio that provides valuable insights into various aspects of app performance, including memory allocation and usage.

Inspect your app’s memory usage with Memory Profiler

Inspect your app’s memory usage with Memory Profiler

By utilizing the Android Profiler, we detect memory leaks in real-time, identify memory usage patterns, and analyze the behavior of the app’s memory over time. The Android Profiler presents information such as memory allocations, native size, shallow size, and retained size, helping to understand memory usage.

WHAT ARE THOSE SIZES?

  • Allocations: This metric refers to the number of objects allocated by our application during a specific time frame. By monitoring allocations, we can identify excessive object creation that might contribute to memory leaks.
  • Native Size: The Android Profiler helps us understand the memory consumed by native resources, including memory allocated outside the Java heap, such as libraries or system components.
  • Shallow Size: represents the memory consumed by an object itself, excluding any referenced objects. By analyzing shallow sizes, we can identify memory-intensive objects that may contribute to memory leaks.
  • Retained Size: The retained size indicates the total memory that would be freed if an object and all its referenced objects were eligible for garbage collection. This metric helps us pinpoint the root causes of memory leaks.

GC ROOTS? WHAT IS THAT?

GC Roots in Android Profiler

GC Roots in Android Profiler

GC Roots, short for Garbage Collector Roots, refers to a set of objects that are considered starting points for the garbage collection process. These objects are in use and not eligible for garbage collection. To understand memory leaks better, it’s essential to look at the concept of GC roots. GC root objects form a root set from which the garbage collector traces the object graph and identifies objects that are no longer reachable. By starting from the GC roots Garbage Collector traverses the object graph.

GC root objects can include:

  • Active threads: Threads that are currently running in the application.
  • Static variables: Static variables defined in classes.
  • JNI (Java Native Interface) references: References from native code to Java objects.
  • System classes: Objects from the Java runtime system.
  • Local variables and input parameters in native code: Objects referenced from native code.

By identifying these GC root objects, we gain insight into the objects that are still in use and understand their impact on memory management.

GC ROOTS AND THE GRAPH STRUCTURE

GC Roots Graph

GC Roots Graph

The GC root objects, along with the references they hold, form a graph-like structure. By following references from the GC root objects, the garbage collector can determine which objects are still in use and which can be garbage collected.

The garbage collector begins its journey from these GC root objects, traversing the object graph and marking reachable objects as “alive” while collecting the rest. This process ensures that memory is efficiently managed and reclaimed.

DEPTH: THE SHORTEST PATH FROM GC ROOTS

You can see the Depth column on Android Profiler. So, what is that?

cr. Android Developer IO

cr. Android Developer IO

The “depth” value represents the number of steps required to traverse the reference graph from the GC roots to a specific object as you can see in the above graphic. A higher depth value implies that the object is indirectly referenced through multiple levels of references.

If the depth of an object is 1, it means that the object is directly referenced by the GC roots which can be a signal for a potential Memory Leak. If the depth is higher than 1, it indicates that the object is indirectly referenced.

What did we say? Finding the starting point- The GC Roots! Analyzing the depth of objects can help us to find the source of memory leaks and understand the reference paths that prevent objects from being garbage collected.

FALSE POSITIVES

While memory profiling tools are effective in detecting memory leaks, there are certain scenarios where false positive results may occur.

tenor.com

tenor.com

It’s important to be aware of these scenarios to avoid unnecessary investigation and potential confusion. Some common false positive scenarios include:

  • Retained Fragments: Fragments can be marked as “retained” during configuration changes to retain their instance across activity recreation. Although they may trigger memory leak warnings, these retained fragments are intentional and not actual leaks.
  • Fragment Transactions: Fragment transactions, such as adding, replacing, or removing fragments, can sometimes trigger false positive memory leak warnings. The memory leak detector may interpret the transaction-related references as potential leaks.
  • Fragment Back Stack: Fragments can be added to a back stack and later popped off when navigating back. The memory leak detector may mistakenly interpret references held by fragments in the back stack as leaks.
  • Fragment View References: Fragment views are commonly referenced through the getView() method. In some cases, the memory leak detection mechanism may flag these view references as leaks, especially if they are held by long-lived objects like custom views or background tasks.

OMG LEAK CANARY? BY SQUARE???

Another powerful Memory Leak detector tool is a **LeakCanary by Square**.

LeakCanary github

LeakCanary github

LeakCanary is an open-source library that specializes in detecting and analyzing memory leaks in Android applications. And it’s so easy to use! Just go to their github documentation, grab the dependency, and paste it on your grade file. That’s it! That’s all you have to do!

dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
}

It integrates seamlessly with your application, providing automatic leak detection and generating comprehensive leak traces.

HOW DOES IT WORK?

When LeakCanary is integrated into our application, it hooks into the Android lifecycle, automatically detecting when activities and fragments are destroyed and should be garbage collected. It holds weak references to these objects and waits for them to be cleared. If the weak reference held by LeakCanary isn’t cleared after a certain period of time (For example: after onDestroy()) and garbage collection runs, the watched object is considered retained and potentially leaking.

LeakCanary automatically detects leaks for various objects, including destroyed Activity instances, destroyed Fragment instances, destroyed fragment View instances, and cleared ViewModel instances. It captures the heap dump when a leak is detected and provides a detailed leak trace, which shows the reference chain leading to the leaking object. Once a destroyed object is detected, such as an activity or a fragment, LeakCanary passes it to an ObjectWatcher. This ObjectWatcherholds weak references to these destroyed objects.

You can watch any objects that are no longer needed, for example, a detached view:

AppWatcher.objectWatcher.watch(myDetachedView, "View was detached")

Conclusion

Our adventure as Leak Hunters in the realm of Android profiling has come to the end. Armed with tools such as the Android Profiler and LeakCanary, we are well-equipped to identify Memory leaks and ensure the stability and performance of our applications.

Remember, the adventure of leak hunting is an ongoing process, as new features and code changes may introduce potential memory leaks. Stay vigilant, and continue refining your app’s memory management practices.

Happy hunting, fellow Leak Hunters!

tenor.com

tenor.com

References:


메타데이터
post_id
552ccff6d82
slug
leak-hunters-adventure-in-android-profiling-552ccff6d82
url
https://medium.com/@aytajd/leak-hunters-adventure-in-android-profiling-552ccff6d82
canonical_url
https://medium.com/@aytajd/leak-hunters-adventure-in-android-profiling-552ccff6d82
author_url
https://medium.com/@aytajd
status
ok
fetched_at
2026-08-10 00:05:03