Thuy's Blog

An easy misuse of WeakReference with Kotlin property

June 17, 2018

You know the lapsed listener problem? Generally, if you don’t want to unregister observers properly by yourself, an idea is to use weak references. However, you’ll have to be very careful not to let garbage collector clean up the weak references unintentionally. In following example, I’ll show you a scenario where it’s quite easy to misuse weak references in Kotlin. And in order to find out what has caused the problem, it may cost a great deal amount of effort and time to debug. All source code can be found here.

An advise from the earlier wikipedia article:

This can be prevented by the subject holding weak references to the observers, allowing them to be garbage collected as normal without needing to be unregistered.

Here comes a sample Publisher that follows that advise:

typealias Event = Int
typealias Callback<T> = (T) -> Unit

class Publisher {
  private var _callback: WeakReference<Callback<Event>?>? = null

  var callback: Callback<Event>?
    get() = _callback?.get()
    set(value) {
      _callback = WeakReference(value)
    }

  fun publish(event: Event) {
    println("Published ⟶ $event")
    _callback?.get()?.invoke(event)
  }
}

Another typical example of using WeakReference-based observers is SharedPreferences from Android SDK. If you implement a subscriber like below, there’s gonna be a problem. At some point, you’ll notice that the subscriber will stop receiving events from the publisher.

class Subscriber(publisher: Publisher) {
  var events: List<Event> = emptyList()

  init {
    publisher.callback = {
      events += it
      println("Received ⟵ $it")
    }
  }
}

I’ll write a simple unit test to reproduce the problem (The test is written in Spek):

object WeakRefMisuseSpec : Spek({
  val publisher = Publisher()
  val subscriber = Subscriber(publisher)

  given("a publisher and a subscriber") {
    it("should be that the subscriber should receive all events from the publisher") {
      (0..5).forEach {
        publisher.publish(it)
      }

      // Attempt to clean up weak references.
      System.gc()

      (6..10).forEach {
        publisher.publish(it)
      }

      assertThat(subscriber.events).containsExactlyElementsOf((0..10))
    }
  }
})

That test will fail:

Failures (1):
  Spek:thuy.weakrefmisuse.WeakRefMisuseSpec:given a publisher and a subscriber:it should be that the subscriber should receive all events from the publisher
    => java.lang.AssertionError:
Expecting:
  <[0, 1, 2, 3, 4, 5]>
to contain exactly (and in same order):
  <[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]>
but could not find the following elements:
  <[6, 7, 8, 9, 10]>

Let’s have a look at the log printed out:

Published ⟶ 0
Received ⟵ 0
Published ⟶ 1
Received ⟵ 1
Published ⟶ 2
Received ⟵ 2
Published ⟶ 3
Received ⟵ 3
Published ⟶ 4
Received ⟵ 4
Published ⟶ 5
Received ⟵ 5
Published ⟶ 6
Published ⟶ 7
Published ⟶ 8
Published ⟶ 9
Published ⟶ 10

Somehow after invoking System.gc(), the subscriber no longer received numbers from 6 to 10. That’s because there’s no strong reference to the Callback object in the init body:

  init {
    // This means `publisher.callback` holds a weak reference
    // to the object initialized in the lambda expression below.
    // This object has only one single weak reference to it.
    // Thus, it will be susceptible to be GC-ed.
    publisher.callback = {
      events += it
      println("Received ⟵ $it")
    }
  }

As a result, that Callback object will be garbage-collected after System.gc(). How about using Kotlin property to hold the Callback object?

class Subscriber(publisher: Publisher) {
  var events: List<Event> = emptyList()

  init {
    publisher.callback = callback
  }

  private val callback: Callback<Event>
    get() = object : Callback<Event> {
      override fun invoke(event: Event) {
        events += event
        println("Received ⟵ $event")
      }
    }
}

The problem is still there. The test has still failed with the exact same error. Why? It looks like the subscriber already holds a strong reference to the callback right? Unfortunately, no. Really? To understand better, let’s try to convert Subscriber back to Java (How? This):

public final class Subscriber {
   @NotNull
   private List events;

   @NotNull
   public final List getEvents() {
      return this.events;
   }

   public final void setEvents(@NotNull List var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.events = var1;
   }

   private final Function1 getCallback() {
      return (Function1)(new Function1() {
         public void invoke(int event) {
            Subscriber.this.setEvents(CollectionsKt.plus((Collection)Subscriber.this.getEvents(), event));
            String var2 = "Received ⟵ " + event;
            System.out.println(var2);
         }

         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke(Object var1) {
            this.invoke(((Number)var1).intValue());
            return Unit.INSTANCE;
         }
      });
   }

   public Subscriber(@NotNull Publisher publisher) {
      Intrinsics.checkParameterIsNotNull(publisher, "publisher");
      super();
      this.events = CollectionsKt.emptyList();
      publisher.setCallback(this.getCallback());
   }
}

Turns out, the callback property defined by get() = {} in Kotlin is just equivalent to a plain method in Java which is getCallback(). In the constructor of Subscriber, this getCallback() method returns an object which has no strong reference. So this object will also be GC-ed like in the previous implementation with the lambda expression. So my assumption was wrong. In a real project, this was my mistake. A colleague of mine had spent nearly a man-day to figure out why a certain point in a WeakReference-based observer hasn’t been reached. And actually it wasn’t that easy for him to figure out immediately right in the first place. The non-deterministic behavior of garbage collector aso made it harder to investigate the root cause because it made the problem happening randomly. In the beginning, when I wrote the code, it actually worked in my testing. Then sometimes it worked, sometimes it didn’t work, depending on when the garbage collector starts off its work.

So how did he fix the problem? Just simply define the callback property without get():

class Subscriber(publisher: Publisher) {
  var events: List<Event> = emptyList()

  private val callback: Callback<Event> = object : Callback<Event> {
    override fun invoke(event: Event) {
      events += event
      println("Received ⟵ $event")
    }
  }

  init {
    publisher.callback = callback
  }
}

When I ran the test again, this time it passed. Why can that code solve the problem? Have a look at the corresponding Java code again, we can easily spot that the callback property now becomes a Java field which is strongly held by the subscriber:

public final class Subscriber {
   @NotNull
   private List events;
   private final Function1 callback;

   @NotNull
   public final List getEvents() {
      return this.events;
   }

   public final void setEvents(@NotNull List var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.events = var1;
   }

   public Subscriber(@NotNull Publisher publisher) {
      Intrinsics.checkParameterIsNotNull(publisher, "publisher");
      super();
      this.events = CollectionsKt.emptyList();
      this.callback = (Function1)(new Function1() {
         public void invoke(int event) {
            Subscriber.this.setEvents(CollectionsKt.plus((Collection)Subscriber.this.getEvents(), event));
            String var2 = "Received ⟵ " + event;
            System.out.println(var2);
         }

         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke(Object var1) {
            this.invoke(((Number)var1).intValue());
            return Unit.INSTANCE;
         }
      });
      publisher.setCallback(this.callback);
   }
}

Always be careful with WeakReferences.


Thuy Trinh

Written by Thuy Trinh who lives and works in Frankfurt, Germany building robust Android apps. You should follow him on Twitter