Unusual Fragment/View Lifecycle Interplay, Rx Leaks and Dependency Leaks
This text is a continuation of half 1 of this mini cookbook sequence on reminiscence leaks for Android. Speaking about reminiscence leaks typically implies greater than the technical downside itself.
For a priority, the definition of a reminiscence loss is subjective. the authors of Programming Android with Kotlin: Reaching structured concurrency leans extra in the direction of a precautionary stance on what constitutes a reminiscence leak, particularly because it pertains to bigger code bases:
- When the heap is stored in allotted reminiscence extra time than needed
- When an object is allotted in reminiscence however is unattainable for the working program
For one more concern, typically OOM spray scans can point out a symptom of the particular downside: that the app was already taking on a lot of the allotted reminiscence on the machine..
Think about these nebulous issues as the topic of the subsequent set of efficiency hits on the scorecard beneath:
6. Statically-saved thread primitives inside singletons → take away
7. Listeners + View members in Fragment → nullify in onDestroyView
8. Rx leaks -> return outcomes to most important thread + clear disposables
6. Statically Saved Thread Primitives Inside Singletons → take away
This instance is introduced inside the context of Dagger 2/Hilt, however the ideas behind this reminiscence leak might be utilized to any type of dependency injection.
Think about the next state of affairs, the place TopologicalProcessor
incorporates a static member reference to a ThreadPoolExecutor
:
As defined within the documentation,@Singleton
the annotation is definitely a scope. The scope determines how lengthy a dependency stays energetic. Within the case of an object annotated with @Singleton
it stays alive for the lifetime of the part through which it may be used.
What makes this a reminiscence leak? the @Singleton
the annotation may very well be seen as a “god object”, so what does it matter that the ThreadPoolExecutor
would all the time exist within the lifetime of the heap? The reply lies in what number of duties are stored contained in the ThreadPoolExecutor.
Suppose we inject the dependency TopologicalProcessor
in each Primary exercise and a few occasion of Second exercise so we will feed duties to load map tiles into tileThreadPoolExecutor
at initialization.
At runtime, a consumer is sitting at 1) Primary exercise display screen, 2) open an occasion of Second exercise3) shut it by navigating again, then 4) open one other occasion of Second exercise another time.
Analyzing the abbreviated log to see what Runnable
Duties are saved and executed in tileMapThreadPoolExecutor
queue, we will see 3 duties created shortly after MainActivity
begins and runs instantly after. After SecondActivity
which provides its personal set of Runnable
queue duties. However when accessing tileMapThreadPoolExecutor
tail, we discover that the tail continues to cling to the identical Runnable
already added duties MainActivity
. The result’s to re-execute all of the duties, despite the fact that we had already executed them earlier than and every process ought to have been discarded as soon as the work is completed.
We’re already seeing issues, however let’s preserve studying till the final log block. SecondActivity
is destroyed, then one other occasion of SecondActivity
It is began. The brand new SecondActivity
provides its personal set of executable duties to the queue. Nonetheless, the queue didn’t eliminate the opposite duties that have been already executed and have been subsequently included collectively when attempting to flush the queue. As we will see, this downside can get costly in a short time.
Keep away from saving Android knowledge threading primitives utilizing astatic
key phrase in Java or in a companion object
in Kotlin. We make No You need threads that reside endlessly and that GC cannot throw away!
The elimination of the static
key phrase, or transfer the category member out companion object
supplies a straightforward resolution to the issue, as proven within the code snippet beneath:
now we have now moved tileMapThreadPoolExecutor
outdoors companion object
. Operating the identical set of interactions (opening a SecondActivity occasion, closing it, after which opening a brand new SecondActivity occasion), we will now see within the log that the duties that have been accomplished have been additionally cleared from reminiscence.
We now see that there are not any duplicate duties working on each opening of an Exercise class. As a result of every Runnable is flushed within the queue after the job completes, flushing tileMapThreadPoolExecutor
it is not going to contain activating pointless threads for work that has already accomplished.
For the fortunate few who can discover them of their code bases, this resolution returns a lot reminiscence that it is going to be exhausting to not declare this a win, so watch out to measure the heap earlier than and after!
7. Listeners + Views in Fragment → dereference in Fragment::onDestroyView
Don’t delete view references in Fragment::onDestroyView
causes these views to be endured utilizing the again stack. This won’t be a giant deal for smaller apps, however giant apps may find yourself with these little leaks piling up and inflicting OOM.
At one level, this was not clear within the documentation: nonetheless, that is the meant conduct that Android builders are anticipated to learn about: A Fragment’s View (however not the Fragment itself) is destroyed when a Fragment is positioned within the again stack. Because of this, builders are anticipated to take away or dereference views in Fragment::onDestroyView
.
As you’ll be able to see, reminiscence leaks are hotly debated: on this case, Programming Android with Kotlin the truth is, I would contemplate this a reminiscence leak, because the views aren’t cleaned up till the Fragment itself is completely destroyed. Fortuitously, there’s a straightforward resolution for this: override all View
members inside a Fragment class in onDestroyView
.
Equally, show bindings and listeners declared as members of the category should even be overridden in Fragment::onDestroyView
. With a code change that consumes as little time and danger as doable, it is a large win for little value and energy price bragging about.
8. Rx leaks -> return outcomes to most important thread + take away throwables with lifecycle
Working with RxJava might be tough. For the sake of dialog, we follow the RxJava 2 context. There are two easy guidelines when working with CompositeDisposable
, which might be lined with the next code instance that exhibits a CompositeDisposable sitting inside a presenter layer. This instance solely exhibits working with a throwaway, however our reminiscence leaks exist already irrespective of how brief. Are you able to determine the 2 sources of leaks?
1. Return the outcomes of the occasion stream to the principle thread on the finish of the Rx chain – in any other case your reworked consequence may find yourself floating to the underside of the background threads and lose reminiscence together with it (or worse, crash).
including .observeOn(AndroidSchedulers.mainThread())
ensures that the outcomes of the heavy lifting can be utilized for the view state:
two. Get rid of disposables. Unsubscribe out of your subscriptions. If we need to subscribe to a CompositeDisposable
inside the context of some Android part, be certain that to delete the subscription on the finish of life to keep away from leaks.
Within the case of our present code snippet, we do the clear
name our CompositeDisposable when the View hooked up to the presenter has completed its lifespan.
Did you see any of those easy adjustments to your code base? If that’s the case, you’ll be able to repair your individual reminiscence leak and verify for variations in reminiscence consumption by doing a .hprof
recording with Reminiscence Profiler in Android Studio! You may as well import your .hprof
recording to dig deeper with Eclipse’s reminiscence analyzer, or select to discover different open supply efficiency instruments like Perfetto, and many others.
You need to perceive the mechanisms of ThreadPoolExecutor
and different knowledge threading primitives? Do you perceive the quirks of conflicting lifecycles in Android parts?
For those who appreciated this text, you could find extra detailed issues on Android efficiency and reminiscence administration round concurrency within the lately printed Android Programming with Kotlin: Reaching Structured Concurrency with Coroutines.
This sequence of articles can be linked to the presentation of Droidcon NYC 2022 Reminiscence Leaks and Efficiency Issues: A Cookbook.
– Performance Considerations for Memory leaks: An Android Cookbook Part 2 | by mvndy | Oct, 2022