Reporting memory leaks in APIs on Android

It’s easy to leak memory in Android as we often hold references to Views and Activities; when developing do you find the LeakCanary popup annoying?

Image for post
Image for post

If you are developing an API where your clients must bind and unbind with their observers, then wouldn’t it be better if you could reduce the chance of misuse?

A typical API might look like the following code where the bind() function would typically store the observer in a list and unbind() remove it.

val observers = mutableListOf<Observer>()

fun bind(observer: Observer) {
observers += observer
}

fun unbind(observer: Observer) {
observers -= observer
}

The problem here though is if your client doesn’t call unbind() then your list will never release the observer, hence causing a memory leak.

If instead, we make the bind() function return an “Unbinder” object then we can add a @CheckResult annotation from the android support-annotations library which gives a warning, “The result of bind is not used”, when the return value is not referenced.

val observers = mutableListOf<Observer>()

@CheckResult
fun bind(observer: Observer): Unbinder {
observers += observer

return object : Unbinder {
override fun unbind() {
observers -= observer
}
}
}

interface Unbinder {
fun unbind()
}

Of course, you can easily circumvent the compiler warning and still readily forget to call unbind(), leading to a leaked observer again.

A popular option is to turn the reference to the Observer in the map to a WeakReference:

val observers = mutableListOf<WeakReference<Observer>>()

@CheckResult
fun bind(observer: Observer): Unbinder {
return WeakReference(observer).let { ref ->
observers
+= ref

object : Unbinder {
override fun unbind() {
observers -= ref
}
}
}
}

Although our Observer should no longer leak, there’s still nothing to ensure your client calls the unbind() function. How can we inform your client they are misusing your API?

A little-known feature of a WeakReference is the ability to provide a ReferenceQueue in the constructor. This queue gets automatically populated when the referent object of the WeakReference is garbage collected. By setting up a WeakReference with a ReferenceQueue to the Unbinder object, we have a mechanism to determine our Unbinder has been garbage collected and thus unbind() not called.

ReferenceQueue has a remove() function that, as documented, “Removes the next reference object in this queue, blocking until one becomes available”. It returns the WeakReference, without the object it refers which will have been garbage collected, but blocks the current thread while it awaits the next reference. You create a queue as follows:

val referenceQueue: ReferenceQueue<Any> = ReferenceQueue()

We can wrap the weak reference, so we have access to a unique identifier as well as a lambda to execute when garbage collected.

class TrackedReference(val uuid: UUID, referent: Unbinder, val operation: () -> Unit) :
WeakReference<Any>(referent, referenceQueue)

The unique identifier helps us store and access our weak references, avoiding garbage collection.

val trackers: MutableMap<UUID, TrackedReference> = Collections.synchronizedMap(mutableMapOf())

As the remove() function is blocking we must poll it from a separate thread; I’ve chosen coroutines in this implementation. To ensure your app shuts down correctly, it is essential this is a daemon thread.

launch(newSingleThreadContext("Reaper Thread")) {
while (true) {
try {
// Once a referent is to be GC'd then the Tracker will be available in the referenceQueue
(referenceQueue.remove() as TrackedReference).apply {
// Clear the tracker
trackers.remove(uuid)
// and execute the operation
operation()
}
} catch (ignored: InterruptedException) {

}
}
}

Lastly, to generate the Unbinder, we merely create a new instance and create a weak reference to it. The unbind() function removes the weak reference, so it is garbage collected before calling an unbindOperation lambda.

@CheckResult
fun bind(unbindOperation: () -> Unit): Unbinder {
// generate exception in advance so stack trace points to
// the bind() function who's Unbinder wasn't called
val exception = IllegalStateException("unbind not called")

val uuid = UUID.randomUUID()

return object : Unbinder {
override fun unbind() {
trackers.remove(uuid)?.clear()
unbindOperation()
}
}.apply {
trackers
[uuid] = TrackedReference(uuid, this) {
// call exception handler with the generated exception
exceptionHandler(exception)
unbindOperation()
}
}
}

We can also call an exception handler when the Unbinder is garbage collected as well as clean up and perform the unbind operation that was previously missing.

Detecting objects being garbage collected like this is not without its faults. Firstly, there is no guarantee that the object will ever be garbage collected; However, these days Android tends to be somewhat aggressive with its memory management. Secondly, it is also possible that the Unbinder might not be garbage collected if the class that holds it cannot be garbage collected for some other reason. Thirdly, creating an exception object as I do means we are creating what might be an unnecessary allocation and as such, I make no claims about the performance of this technique.

Hopefully, this has given you an insight into an under-used feature of WeakReferences and although I wouldn’t necessarily recommend using it in a production app the technique certainly could help during development.

Source code available on GitHub:

Matt Dolan has been eating doughnuts and developing with Android since the dark days of v1.6.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store