
Atypical Fragment/View lifecycle interplay, Rx leaks, and dependency leaks
This newsletter is a continuation of phase 1 of this mini collection of reminiscence leaks cookbook for Android. Speaking about reminiscence leaks on occasion comes to greater than the technical downside itself.
For one worry, the definition of a reminiscence leak is subjective. The authors of Programming Android with Kotlin: Reaching Structured Concurrency leans extra against a cautionary stance on what constitutes as a reminiscence leak, particularly with reference to greater codebases:
- When the heap holds to allotted reminiscence longer than vital
- When an object is allotted in reminiscence however is unreachable for the working program
For every other worry, on occasion the OOMs sprinkling analytics would possibly point out a symptom to the true downside — that the applying used to be already taking over lots of the reminiscence allotted within the tool.
Believe those nebulous issues because the theme of the following set of efficiency hits within the ranking card beneath:
6. Statically-saved thread primitives inside singletons → take away
7. Listeners + View participants in Fragment → nullify in onDestroyView
8. Rx leaks -> go back effects to primary thread + transparent disposables
6. Statically-saved thread primitives inside singletons → take away
This situation is gifted throughout the context of Dagger 2/Hilt, however the ideas at the back of this reminiscence leak may also be implemented to any type of dependency injection.
Believe the next state of affairs, the place TopologicalProcessor
holds a static member connection with a ThreadPoolExecutor
:
As defined in documentation,@Singleton
annotation is if truth be told a scope. Scope determines how lengthy a dependency is stored alive. In relation to an object annotated with @Singleton
, it’s stored alive for the life of the element it may well be utilized in.
What makes this a reminiscence leak? The @Singleton
annotation may well be observed as a “God object”, so what does it subject that the ThreadPoolExecutor
would at all times exist within the life of the heap? The solution lies in what number of duties are stored inside ThreadPoolExecutor.
Assume we inject the dependency TopologicalProcessor
in each MainActivity and a few example of SecondActivity so we will feed duties to load map tiles into tileThreadPoolExecutor
at initialization.
At runtime, a person is sitting at the 1) MainActivity display screen, 2) opens an example of SecondActivity, 3) closes it by way of navigating again, then 4) opens every other example of SecondActivity another time.
Upon analyzing abridged logging to peer what Runnable
duties are saved and completed in tileMapThreadPoolExecutor
queue, we will see 3 duties created in a while after MainActivity
begins and run promptly after. Then SecondActivity
, which provides its personal set of Runnable
duties to the queue. However upon having access to tileMapThreadPoolExecutor
queue, we realize that the queue continues to carry directly to the similar Runnable
duties already added from MainActivity
. The result’s executing all of the duties once more, even supposing we had already run the duties previous and each and every activity must were disposed of after the paintings had finished.
We’re already seeing issues, however let’s stay studying all the way down to the ultimate block of logging. SecondActivity
is destroyed, then every other example of SecondActivity
is began. The brand new SecondActivity
provides its personal set of runnable duties to the queue. Alternatively, the queue has no longer disposed of the opposite duties which has already run, and therefore incorporated in combination when making an attempt to drain the queue. As we will see, this downside can turns into dear in no time.
Keep away from saving Android knowledge threading primitives the use of astatic
key phrase in Java or in a spouse object
in Kotlin. We do no longer need forever-living threads which can’t be disposed of by way of GC!
Disposing of the static
key phrase, or transferring the category member out of doors spouse object
supplies a very simple repair to the problem, as proven within the code snippet beneath:
Now we have now moved tileMapThreadPoolExecutor
out of doors of spouse object
. Upon working the similar set of interactions — opening one example of SecondActivity, final it, then opening a brand new example of SecondActivity — we will now see in logging that duties which have been finished have additionally been cleared in reminiscence.
We now see there aren’t any reproduction duties working at each and every opening of an Task magnificence. As a result of each and every Runnable is disposed within the queue after the paintings is entire, emptying tileMapThreadPoolExecutor
received’t contain spinning up unnecessary threads for paintings this is already finished.
For the fortunate few who would possibly to find those of their code bases, this repair offers again such a lot reminiscence, it’s going to be onerous to to not claim this a win — so take care to measure heap ahead of and after!
7. Listeners + Perspectives in Fragment → nullify references in Fragment::onDestroyView
No longer clearing view references in Fragment::onDestroyView
reasons those perspectives to be retained the use of the again stack. This will not be a large deal for smaller packages, however huge packages would possibly finally end up having the ones small leaks gather and reason OOMs.
At one time, this used to be no longer transparent in documentation: alternatively, that is supposed habits Android builders are anticipated to grasp: a Fragment’s View (however no longer the Fragment itself) is destroyed when a Fragment is put at the again stack. Because of this, builders are anticipated to transparent/nullify references for perspectives in Fragment::onDestroyView
.
As you’ll see, reminiscence leaks are hotly debated: on this case, Programming Android with Kotlin would certainly believe this a reminiscence leak, since perspectives don’t seem to be wiped clean up till the Fragment itself is completely destroyed. Happily, there is a straightforward repair for this — nullifying all View
participants inside a Fragment magnificence on onDestroyView
.
Likewise, view bindings and listeners declared as magnificence participants must even be nullified in Fragment::onDestroyView
. With a code trade as little time intake and possibility as conceivable, it’s a large win for little price and energy value appearing off.
8. Rx leaks -> go back effects to primary thread + transparent disposables with lifecycle
Operating with RxJava may also be difficult. For the sake of dialog, we keep on with RxJava 2 context. There’s two simple laws when running with CompositeDisposable
, either one of which may also be coated with the next code instance appearing a CompositeDisposable sitting inside a presenter layer. This situation handiest presentations running with one disposable, however our reminiscence leaks exist already as brief as it’s. Are you able to spot the 2 assets of leaks?
1. Go back the result of the development movement again to the primary thread on the finish of the Rx chain — another way, your reworked outcome would possibly finally end up floating off within the nethers of background threads, and leaking reminiscence proper at the side of it (or worse, crashes).
Including .observeOn(AndroidSchedulers.mainThread())
guarantees the result of the heavy paintings is usable for view state:
2. Get rid of the disposables. Unsubscribe on your subscriptions. If we need to subscribe to a CompositeDisposable
throughout the context of a few Android element, remember to transparent the subscription on the finish of the lifecycle to stop leak.
In relation to our present code snippet, we make the transparent
name for our CompositeDisposable when the View connected to the presenter has ended its lifestyles.
Did you notice any of those simple adjustments to your code base? If that is so, you’ll repair your individual reminiscence leak and take a look at for variations in reminiscence intake by way of making an .hprof
recording with the Reminiscence Profiler in Android studio! You’ll be able to additionally import your .hprof
recording to drill down deeper with Eclipse’s Reminiscence Analyzer, or make a choice to discover different open supply efficiency tooling corresponding to Perfetto, and so on.
Need to perceive the mechanisms of ThreadPoolExecutor
and different knowledge threading primitives? Perceive the quirks of clashing lifecycles in Android parts?
In the event you appreciated this newsletter, you’ll to find extra in-depth concerns for Android efficiency and reminiscence control round concurrency within the newly revealed Programming Android with Kotlin: Reaching Structured Concurrency with Coroutines.
This newsletter collection may be tied to the Droidcon NYC 2022 Presentation Reminiscence Leaks & Efficiency Concerns: A Cookbook.