A quick story about async callbacks, memory leaks, WeakReferences, and misconceptions

Gabor Varadi
ProAndroidDev
Published in
9 min readNov 27, 2018

--

Using WeakReference<Context> is just as hideous as this color scheme.

My co-worker told me the following: “You said there can be a memory leak here. I looked for a solution and saw that we can use a WeakReference<Context> to wrap the Activity context, that way it won’t leak.”

To which I said, “that’s great, except that’s a hack, and it doesn’t solve the real problem, so don’t.”

And then he said: “But I don’t understand what you mean. Show me an article that explains the real problem you talk about and why I shouldn’t use a WeakReference”.

Once I explained the real problem, he said “this is a different problem, but now I know why we shouldn’t use WeakReference”.

— — — — — — — — — — — — —

But that brings us to here, because apparently people managed to NEVER write an article about why you shouldn’t use WeakReference<Context> (or WeakReference<Activity>, or WeakReference<MyContract.View>) to “fix” your memory leaks (related to configuration changes and asynchronous callbacks, AsyncTask and the like)!

In fact, the only articles that mention that this is not the correct approach are comments by Vasiliy Zukanov on some Medium posts.

So let’s embark on a journey and assess why using a WeakReference is not the right solution, and what other options we have to solve the memory leak and the real problem.

— — — — — — — — — — — — —

By the way, what’s a memory leak in the context of Android anyway?

For those that don’t know, it can happen when you have a reference to an Activity context in something that outlives the Activity. The Activity starts to change configurations — is mercilessly torn down and recreated — but the previously existing and held reference keeps the Activity context and all its inflated Views alive, the garbage collector cannot finalize them.

Of course, this also applies if the Activity is registered to a global bus, but it’s never unregistered, and the Activity is finished. Also happens if there is an uncleared static reference to the Activity. All of these can cause a memory leak.

There’s a pretty cool tool out there called LeakCanary that one can use to detect these scenarios.

— — — — — — — — — — — — —

Options for resolving memory leaks caused by holding a reference to Activity:

#(-1).) WeakReference

Every single guide and article and library and answer on Stack Overflow you find talks about how you should just use a WeakReference and you’re done!

// from stack overflow
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);

final ImageView imageView = mImageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}

…Sure, you’re done if you don’t care about the results of the asynchronous operation. Was it a success? Was it a failure? The reference is null, we certainly don’t care about the result enough to handle it, or notify the user about the results for that matter. (And whether it’s null depends on whether the GC has already finalized it.)

Now if you DON’T want to just flat-out ignore the results of a costly network request, you might want to do something about it. Maybe the error you get is important, and you don’t want to swallow it. Maybe even show a dialog to let the user know about it.

We then need a solution that actually handles this scenario: for example, emitting an event, and letting the Activity register/unregister for handling this event, and enqueueing these events while the Activity is not available.

#(+1).) Using an EventBus that is paused while the Activity is not resumed, and enqueues events while paused

This is the approach and code (with some Java 8 flavoring) that I was using in 2015, which solved this problem in a fairly simple manner — it was required because we were using Android Priority JobQueue, so we had to emit events via an EventBus from the tasks anyway.

public class SingletonBus {
private final Bus bus;
SingletonBus() {
this.bus = new Bus(ThreadEnforcer.ANY); //yes I know
}
private Bus bus;

private volatile boolean paused;
private final Vector<Object> eventQueueBuffer = new Vector<>();
private Handler handler = new Handler(Looper.getMainLooper());

public <T> void postToMainThread(final T event) {
if(paused) {
eventQueueBuffer.add(event);
} else {
handler.post(() -> {
bus.post(event);
});
}
}

public <T> void register(T subscriber) {
bus.register(subscriber);
}

public <T> void unregister(T subscriber) {
bus.unregister(subscriber);
}

public boolean isPaused() {
return paused;
}

public void setPaused(boolean paused) {
this.paused = paused;
if(!paused) {
Iterator<Object> eventIterator = eventQueueBuffer.iterator();
while(eventIterator.hasNext()) {
Object event = eventIterator.next();
postToMainThread(event);
eventIterator.remove();
}
}
}
}

And it actually worked just fine for our purposes at the time; namely that if the Activity was being rotated, events would get enqueued, and when the Activity was resumed again, we would receive them.

We want the exact same behavior with more up to date mechanisms.

#(-0.5).) LiveData<EventWrapper>

According to Medium, we can replace SingleLiveEvent with LiveData<EventWrapper>, where we can tell the command explicitly that we’ve consumed it, and we no longer want to consume it again. Supposedly ideal for handling an event only once, even if there are multiple observers for it.

This might seem like a great way to show an error message. LiveData is nice, because it provides automatic unsubscription on destroy lifecycle callback, and it holds 1 value that is emitted once an observer subscribes for observing changes, so the Activity receives it even on resubscription.

The problem is that it holds 1 value. It does not enqueue multiple events.

Therefore it can result in loss of events, and isn’t what we actually need here.

#(-2).) Storing the error or the loaded data as a sealed class inside a BehaviorRelay (just like MVI tells you to do)

An awfully common malpractice is to store the state combined together with the data in a single BehaviorRelay, where this combined object tends to look like this:

sealed class HelloWorldViewState {
object LoadingState :
HelloWorldViewState()
data class DataState(val greeting: String) :
HelloWorldViewState()
data class ErrorState(val error: Throwable) :
HelloWorldViewState()
}

What the MVI tutorials don’t tell you is that if you throw this into a BehaviorRelay while you’re either loading new data or you’re showing an error, then on rotation and resubscription, previously received data will be overwritten by said loading/error state.

That makes this approach effectively worse than WeakReferences.

If you want to do this, then consider a LiveData<Resource<T>> instead, much more reliable. Doesn’t overwrite existing loaded data with errors and progress dialogs.

Another additional downside of this “bundle events with state” approach is that (unless your design requires it, of course), rotation will re-trigger the same error event multiple times upon resubscription. So this doesn’t provide the ability to “handle one-off event only once”.

#(+1).) PublishRelay + ObservableTransformers.valve()

Using the powers of RxJava, we can easily emit events to multiple subscribers using Relays.

PublishRelay allows us to emit an event once, and the subscribers that are currently subscribed will receive it, but new subscribers won’t receive it again (so no need for EventWrappers just to store a mutable consumed flag).

Additional benefit is that on Aug 28 2018, Dávid Karnok added ObservableTransformers.valve() to RxJava2Extensions, allowing us to do this:

eventRelay.compose(
ObservableTransformers.valve(isActivityPausedObservable, true)
)

Meaning we can actually enqueue events while the Activity is paused, meaning we most likely don’t have subscribers available. Good news is that valve supposedly does not release events if there are no subscribers (unlike RefCountSubject), and allows multiple subscribers if that’s necessary (unlike UnicastWorkSubject).

If you’ve ever read the code for this operator, you probably didn’t want to write it yourself, especially in retrospect. ;)

#(+1.5).) Command Queue 0.1.1

As I could not find a solution at the time I was looking for one, I created CommandQueue to fit my needs for:

  • supporting only 1 observer at a time
  • enqueueing events while observer is not available
  • ability to explicitly pause the event queue until it is explicitly resumed

And as I couldn’t find anything like this (and we couldn’t get RxJava to do it for us, even though others have), I made CommandQueue, that is about ~100 lines long and surprisingly lame. It’s really a queue and a boolean (and a listener interface). Who knew.

override fun onStart() {
super.onStart()
viewModel.commandQueue.setReceiver { command ->
when (command) {
is MainViewModel.Events.DoSomething ->
showToast("Do something!", Toast.LENGTH_SHORT)
is MainViewModel.Events.DoOtherThing ->
showToast("Do other thing!", Toast.LENGTH_SHORT)
}.safe()
}
}
override fun onStop() {
viewModel.commandQueue.detachReceiver()
super.onStop()
}

With a version count like that, I’m surprised we are relying on it as heavily as we are. It is not thread-safe but it works? Also you don’t need to understand operator fusion to read the source code. We’re using it only on the main thread anyway.

But of course, there are less tacky options out there. I just wanted to give it a mention because if I had known a better option for all these 3 requirements, I would have used that.

#(+1.75).) UnicastWorkSubject

In RxJava2Extensions, there are actually a lot of goodies. Along with Reactive-Streams-spec compatible versions of Single (called Solo), or Completables (called Nono), or Maybe (called Perhaps); what’s more interesting is the additional subject types we get.

One of them is called UnicastWorkSubject, which allows subscribing a new observer once the previous subscriber has unsubscribed, and retains events inbetween.

A Subject that holds an unbounded queue of items and relays/replays it to a single Observer at a time, making sure that when the Observer disposes, any unconsumed items are available for the next Observer.

This Subject doesn't allow more than one Observers at a time.

So if this suits our needs (as we know there will only be one subscriber), then this is a great way to replace WeakReferences with event emission. But if we can’t guarantee a single subscriber, then…

#(+1.25).) DispatchWorkSubject

Apparently a possible solution for emitting/enqueueing events with RxJava from our ViewModels (or whatevers) to our View would be to use a DispatchWorkSubject.

A Subject variant that buffers items and allows one or more Observers to exclusively consume one of the items in the buffer asynchronously. If there are no Observers (or they all disposed), the DispatchWorkSubject will keep buffering and laterObservers can resume the consumption of the buffer.

The way it works is that it allows multiple subscribers, but if there are no subscribers, the events are enqueued until the first subscriber arrives.

So instead of hacking around with retaining only 1 event with LiveData, or just flat out ignoring events emitted when there are no subscribers using a PublishRelay, we could just use this and have our events enqueued until the View resubscribes. Handy!

However, it only dispatches the event to one subscriber (when there are multiple subscribers). That’s quite important to keep in mind, thanks Miguel for warning me and correcting me in the comments below.

#(+2.5).) EventEmitter — the update from the future

Due to how I just couldn’t find a safe way to enqueue events when there are no observers, but still support multiple observers, I’ve created a new library called EventEmitter that internally wraps a command queue, and dispatches its events to multiple observers.

While intended to be observed and written to on a single thread (solves the problems of having to use Rx in incomprehensible ways), it’s still the safest means I’ve found so far to achieve this.

private val emitter: EventEmitter<String> = EventEmitter()
val events: EventSource<String> get() = emitter

fun doSomething() {
emitter.emit("hello")
}

And

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

viewModel = getViewModel<MyViewModel>()
viewModel.events.observe(viewLifecycleOwner) { event ->
// ...
}
}

This allows writing into the event emitter locally, while exposing it as an EventSource (that can only be observed, but not be written to).

The library used is available at https://github.com/Zhuinden/live-event.

— — — — — — — — — — — — — — — —

Addendum: I’ve also heard that LinkedListChannel in Kotlin-Coroutine-Channels is also meant to have specifically this behavior, but I haven’t dabbled with channels yet.

Conclusion

Hopefully this article showed what options are available for properly handling the results of asynchronous event callbacks, instead of silently swallowing them with the super-popular WeakReference approach (which honestly never made sense to use in this particular context anyway).

For proper closure though, I must mention one last possible solution for having to do all this magic just to ensure that our asynchronous callbacks aren’t lost across rotation and we can communicate results back to our UI.

Namely that you can “block” configuration changes for orientation change, and receive a call to onConfigurationChanged(Configuration), and handle it however you see fit, without the Activity being destroyed in the process.

From Chrome OS Resizing Codelabs

This is one of those things that people said for a long time not to do, but on the other hand, they said this about bottom navigation views (vs nav drawers), or overriding onBackPressed() to handle your application’s navigation yourself. After all, if you want to handle floating windows on Chromebooks in such a fancy manner, you’re probably gonna need to use onConfigurationChanged() anyway.

In which case, you no longer need to handle the Activity dying under you just because the user rotated the screen. Win-win.

--

--

Android dev. Zhuinden, or EpicPandaForce @ SO. Extension function fan #Kotlin, dislikes multiple Activities/Fragment backstack.