Blog Infos
Author
Published
Topics
,
Author
Published

This article is a continuation of part 1 of this mini series of memory leaks cookbook for Android. Talking about memory leaks sometimes involves more than the technical problem itself.

For one concern, the definition of a memory leak is subjective. The authors of Programming Android with Kotlin: Achieving Structured Concurrency leans more towards a cautionary stance on what constitutes as a memory leak, especially in regards to larger codebases:

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

For another concern, sometimes the OOMs sprinkling analytics might indicate a symptom to the actual problem — that the application was already taking up most of the memory allocated in the device.

Consider these nebulous concerns as the theme of the next set of performance hits in the score card below:

6. Statically-saved thread primitives within singletons → remove
7. Listeners + View members in Fragment → nullify in onDestroyView
8. Rx leaks -> return results to main thread + clear disposables
6. Statically-saved thread primitives within singletons → remove

This example is presented within the context of Dagger 2/Hilt, but the concepts behind this memory leak can be applied to any form of dependency injection.

Consider the following scenario, where TopologicalProcessor holds a static member reference to a ThreadPoolExecutor:

import java.util.concurrent.*
private val threadFactory = object : ThreadFactory {
private val count = AtomicInteger(1)
override fun newThread(r: Runnable): Thread = Thread(r, "Thread #${count.getAndIncrement()}")
}
@Singleton
class TopologicalProcessor {
/* Removed for brevity */
companion object {
var tileMapThreadPoolExecutor = ThreadPoolExecutor(
corePoolSize = 3,
maximumPoolSize = 3,
keepAliveTime = 0L,
unit = TimeUnit.SECONDS,
workQueue = LinkedBlockingQueue<Runnable>(),
threadFactory = threadFactory
)
}
}

As explained in documentation,@Singleton annotation is actually a scope. Scope determines how long a dependency is kept alive. In the case of an object annotated with @Singleton, it is kept alive for the lifetime of the component it might be used in.

What makes this a memory leak? The @Singleton annotation might be seen as a “God object”, so what does it matter that the ThreadPoolExecutor would always exist in the lifetime of the heap? The answer lies in how many tasks are kept within ThreadPoolExecutor.

Suppose we inject the dependency TopologicalProcessor in both MainActivity and some instance of SecondActivity so we can feed tasks to load map tiles into tileThreadPoolExecutor at initialization.

At runtime, a user is sitting on the 1) MainActivity screen, 2) opens an instance of SecondActivity, 3) closes it by navigating back, then 4) opens another instance of SecondActivity once more.

A visual representation of Activities and continuous active Runnable in queue given the current memory leak.

Upon examining abridged logging to see what Runnable tasks are stored and executed in tileMapThreadPoolExecutor queue, we can see 3 tasks created shortly after MainActivity starts and run promptly after. Then SecondActivity, which adds its own set of Runnable tasks to the queue. But upon accessing tileMapThreadPoolExecutor queue, we notice that the queue continues to hold on to the same Runnable tasks already added from MainActivity. The result is executing all the tasks again, even though we had already run the tasks earlier and each task should have been disposed of after the work had completed.

We’re already seeing problems, but let’s keep reading down to the last block of logging. SecondActivity is destroyed, then another instance of SecondActivity is started. The new SecondActivity adds its own set of runnable tasks to the queue. However, the queue has not disposed of the other tasks which has already run, and subsequently included in aggregate when attempting to empty the queue. As we can see, this problem can becomes expensive very quickly.

Avoid saving Android data threading primitives using astatic keyword in Java or in a companion object in Kotlin. We do not want forever-living threads which cannot be disposed of by GC!

Removing the static keyword, or moving the class member outside companion object provides an easy fix to the issue, as shown in the code snippet below:

import java.util.concurrent.*
private val threadFactory = object : ThreadFactory {
private val count = AtomicInteger(1)
override fun newThread(r: Runnable): Thread = Thread(r, "Thread #${count.getAndIncrement()}")
}
@Singleton
class TopologicalProcessor {
var tileMapThreadPoolExecutor = ThreadPoolExecutor(
corePoolSize = 3,
maximumPoolSize = 3,
keepAliveTime = 0L,
unit = TimeUnit.SECONDS,
workQueue = LinkedBlockingQueue<Runnable>(),
threadFactory = threadFactory
)
/* Removed for brevity */
/* companion object { ... } */
}

We have now moved tileMapThreadPoolExecutor outside of companion object. Upon running the same set of interactions — opening one instance of SecondActivity, closing it, then opening a new instance of SecondActivity — we can now see in logging that tasks that have been completed have also been cleared in memory.

We now see there are no duplicate tasks running at each opening of an Activity class. Because each Runnable is disposed in the queue after the work is complete, emptying tileMapThreadPoolExecutor won’t involve spinning up needless threads for work that is already completed.

A visual representation of user navigation in memory and properly disposing Runnable tasks within the ThreadPoolExecutor queue

For the lucky few who might find these in their code bases, this fix gives back so much memory, it will be hard to not to declare this a win — so take care to measure heap before and after!

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

7. Listeners + Views in Fragment → nullify references in Fragment::onDestroyView

Not clearing view references in Fragment::onDestroyView causes these views to be retained using the back stack. This might not be a big deal for smaller applications, but large applications might end up having those small leaks accumulate and cause OOMs.

At one time, this was not clear in documentation: however, this is intended behavior Android developers are expected to know: a Fragment’s View (but not the Fragment itself) is destroyed when a Fragment is put on the back stack. For this reason, developers are expected to clear/nullify references for views in Fragment::onDestroyView.

As you can see, memory leaks are hotly debated: in this case, Programming Android with Kotlin would indeed consider this a memory leak, since views are not cleaned up until the Fragment itself is permanently destroyed. Luckily, there is an easy fix for this — nullifying all View members within a Fragment class on onDestroyView.

class SomeFragment : Fragment() {
private var imageView: ImageView? = null
/* removed for brevity */
override fun onDestroyView() {
super.onDestroyView()
imageView = null
}
}
view raw SomeFragment.kt hosted with ❤ by GitHub

Likewise, view bindings and listeners declared as class members should also be nullified in Fragment::onDestroyView. With a code change as little time consumption and risk as possible, it’s a big win for little cost and effort worth showing off.

8. Rx leaks -> return results to main thread + clear disposables with lifecycle

Working with RxJava can be tricky. For the sake of conversation, we stick with RxJava 2 context. There’s two easy rules when working with CompositeDisposable, both of which can be covered with the following code example showing a CompositeDisposable sitting within a presenter layer. This example only shows working with one disposable, but our memory leaks already exist as short as it is. Can you spot the two sources of leaks?

class ActivityPresenter @Inject constructor(
private val repository: SomeRepository
) : BasePresenter<ActivityView>() {
/* removed for brevity */
private val viewDisposable = CompositeDisposable()
override fun attachView(view: ActivityView?) {
super.attachView(view)
val disposable = repository.doSomeHeavyWorkAndReturnState()
.subscribeOn(Schedulers.io())
.subscribe { viewState.render(it) }
viewDisposable.add(disposable)
}
}

1. Return the results of the event stream back to the main thread at the end of the Rx chain — otherwise, your transformed result might end up floating off in the nethers of background threads, and leaking memory right along with it (or worse, crashes).

Adding .observeOn(AndroidSchedulers.mainThread()) ensures the results of the heavy work is usable for view state:

class ActivityPresenter @Inject constructor(
private val repository: SomeRepository
) : BasePresenter<ActivityView>() {
/* removed for brevity */
private val viewDisposable = CompositeDisposable()
override fun attachView(view: ActivityView?) {
super.attachView(view)
val disposable = repository.doSomeHeavyWorkAndReturnState()
.subscribeOn(Schedulers.io()) // send work to background thread
.observeOn(AndroidSchedulers.mainThread()) // return results to main thread
.subscribe { viewState.render(it) }
viewDisposable.add(disposable)
}
}

2. Dispose of the disposables. Unsubscribe to your subscriptions. If we wish to subscribe to a CompositeDisposable within the context of some Android component, make sure to clear the subscription at the end of the lifecycle to prevent leak.

In the case of our current code snippet, we make the clear call for our CompositeDisposable when the View attached to the presenter has ended its life.

class ActivityPresenter @Inject constructor(
private val repository: SomeRepository
) : BasePresenter<ActivityView>() {
/* removed for brevity */
private val viewDisposable = CompositeDisposable()
override fun attachView(view: ActivityView?) {
super.attachView(view)
val disposable = repository.doSomeHeavyWorkAndReturnState()
.subscribeOn(Schedulers.io()) // send work to background thread
.observeOn(AndroidSchedulers.mainThread()) // return results to main thread
.subscribe { viewState.render(it) }
viewDisposable.add(disposable)
}
override fun detachView(retainInstance: Boolean) {
super.detachView(retainInstance)
compositeDisposable.clear() // calls to dispose of all disposables end of view life
}
}

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 choose to explore other open source performance tooling such as Perfetto, etc.

Need more content on Android in-depth?

Want to understand the mechanisms of ThreadPoolExecutor and other data threading primitives? Understand the quirks of clashing lifecycles in Android components?

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.

This article series is also tied to the Droidcon NYC 2022 Presentation Memory Leaks & Performance Considerations: A Cookbook.

This article was originally published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu