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
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 a
static 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
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
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).
.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.