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 @Singletonit 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.

A visual representation of the runtime.  The image shows MainActivity and SomeActivity executing as steps 1 and 2, both of which reference the same queue stored in tileMapThreadPoolExecutor in our singleton dependency.  It also shows a leak as the queue holds tasks after the job is complete.
A visible illustration of actions and steady energetic Runnable queued given the present reminiscence leak.

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 SecondActivitywhich 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.

Screenshot showing the log where the tasks in MainActivity and the first instance of SecondActivity are held in the second instance of SecondActivity.

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.

Screenshot showing the MainActivity, SecondActivity, and SecondActivity task log.  All the tasks were killed after executing each one, so we see three tasks being held in the queue and executed

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.

A visible illustration of consumer navigation in reminiscence and the correct elimination of Runnable duties inside the ThreadPoolExecutor queue

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.

Credit score to PY for monitoring down this reminiscence leak

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

By admin

x