How Proper Lifecycle Management Can Prevent Memory Leaks

OutOfMemoryException is a common and frustrating bug, and one of the prime causes of unexpected app shutdown.

“Why is this happening now, if the app was working perfectly yesterday?” It’s a question which perplexes both rookie and advanced Android developers the world over.

There are a variety of potential causes of OutOfMemory Exceptions, but one of the most common is the memory leak — the allocation of memory in the app which is never released. This article will explain how to minimize this risk through effective lifecycle management — a crucial but often overlooked part of the development process.

TL;DR

In this article we’ll discuss:

  • The reasons for memory leakage
  • Basic steps to prevent it
  • More advanced forms of lifecycle management
  • Tools that can manage the lifecycle of your product

Why Memory Leakage Happens

The problem is simple. Certain objects should only have a fixed lifespan, and when their useful life is over, they need to be removed.

In theory, this memory should be disposed of when a process is killed using onStop or onDestroy. However, misuse of the object referencing can prevent the garbage collector from deallocating unused objects. For example: if the unused object A references the unused object B, you will end up with two unnecessary objects that the garbage collector will never dealloc as they are referencing one another.

Common Tricks to Stop This Happening

Developers can take a number of steps to stop dead activities being trapped in memory.

  1. Register/Unregister BroadcastReceivers in onResume()/onPause() or onStart()/onStop()
  2. NEVER use static variables for Views/Activities/Contexts
  3. Singletons that need to hold references to the Context should use applicationContext() or wrap it into WeakReference
  4. Be careful with anonymous and non-static inner classes as they hold an implicit reference to their enclosing class.
    1. Use static inner classes instead of anonymous if they are going to outlive the parent class (like Handlers).
    2. If the inner or anonymous class is cancellable (such as AsyncTask, Thread, RxSubscriptions), cancel it when the activity is destroyed.

The Lifecycle

Once you’ve completed the basic steps above, it’s time for something much more important: The Lifecycle of the app’s activities. If we don’t manage the lifecycle correctly, we’ll end up hanging on to memory when it’s no longer needed.

There are many different tasks involved in this. For each activity, we need to interrupt threads, get rid of subscriptions in RxJava, cancel AsyncTask references and ensure the reference of this activity (and any other activity connected to it) is correctly removed. All these tasks can drain a huge amount of the developer’s time.

Things are made even more complicated by the Model View Presenter (MVP), the architectural pattern often employed to build the user interface in Android. MVP, however, is useful to decouple business logic from the View.

In the MVP pattern, both View and Presenter are abstract implementations of a contract for a behaviour between them. The most common way to implement MVP is using an Activity/Fragment as an implementation for the view and a simple implementation for the presenter who is used to having a reference of the View.

So we end up having a View with a Presenter reference and a Presenter with a View Reference (Hint: We have a potential leak here).

Given these potential difficulties, it’s essential that we put a proper management structure in place to remove the excess memory created during the lifecycle. There are several proven ways of doing this:

1. Creating Lifecycle-Aware Components Using Android Arch Lifecycle

Lifecycle-aware components are smart. They can react to a change in the lifecycle status of another component, such as activities or fragments, by getting rid of memory, for example. This means the code is lighter and much more memory-efficient.

Arch Lifecycle is a new library from Android that offers a set of tools to build lifecycle-aware components. The library works in an abstract way, meaning lifecycle owners no longer have to worry about managing the lifecycle of specific tasks and activities.

The key tools and definitions of Arch Lifecycle are as follows:

  • LifeCycle : A sorting system that defines which objects have an Android lifecycle, and allows them to then be monitored.
  • LifecycleObserver : A regular interface that monitors each object identified as having an Android lifecycle, using a simple formula to handle each key lifecycle event.
  • @OnLifecycleEvent : An annotation that can be used in classes that implement the LifecycleObserver interface. It allows us to set key lifecycle events that will trigger the annotated method whenever launched. Here is a list of all the events that can be set:
    • ON_ANY
    • ON_CREATE
    • ON_DESTROY
    • ON_PAUSE
    • ON_RESUME
    • ON_START
    • ON_STOP
  • LifecycleOwner is implemented by default for every Android component whose lifecycle can be managed, and gives the developer control of each event.

Using these tools, we are able to send all clean tasks to their owners (presenters in our case), so we have a clean, decoupled code free of leaks (at least in the presenter layer).

Here is a super-basic implementation to show you what we’re talking about:

interface View: MVPView, LifecycleOwner

class RandomPresenter : Presenter<View>, LifecycleObserver {
  private lateinit var view: View
  override fun attachView(view: View) {
    this.view = view
    view.lifecycle.addObserver(this)
  }

  @OnLifecycleEvent(Lifecycle.Event.On_DESTROY)
  fun onClear() {
    //TODO: clean 
}

2. Using Android Arch View Models as Presenters and LiveData

Another way to avoid memory leakage through lifecycle mismanagement is by using the new ViewModels from the Arch Components Library.

A ViewModel is an Abstract class that implements a single function known as onClear, which is called automatically when a particular object has to be removed. The ViewModel is generated by the framework and it’s attached to the lifecycle of the creator (as an added bonus, it’s super-easy to inject with Dagger.)

As well as using ViewModel, LiveData also provides a vital channel of communication. The product follows a reactive, easy-to-observe pattern, which means individual objects can observe it (creating less coupled code).

The great point here is that LiveData can be observed by a lifecycle owner, so the data transference is always managed by the lifecycle —and we can ensure that any reference is retained while using them.

3. Using LeakCanary and Bugfender

In addition to the aforementioned steps, we wanted to recommend two important pieces of kit: LeakCanary, a popular tool for monitoring leaks, and our very own Bugfender.

LeakCanary is a memory detection library for Android and Java. It’s open-source, so there’s a huge community behind it, and it doesn’t just tell you about a leak — it informs you of the likely cause.

Bugfender, our remote logging tool, allows you to debug individual LeakTraces and extend a class called DisplayLeakService, which lets us know when a leak is raised. Then we can log it with Bugfender easily.

public class LeakUploadService extends DisplayLeakService {
  override fun afterDefaultHandling(heapDump: HeapDump, result: AnalysisResult, leakInfo: String) {
    if (result.leakFound) {
      Bugfender.d(“LeakCanary”, result.toString())
    }
  }
}

In addition, users get all Bugfender’s other benefits including 24/7 log-recording (even when the device is offline), built-in crash reporting and an easy-to-use web console, which allows drill-down into individual devices for better customer care.

For more on Bugfender, please click here.

29