Ginger Binger

Lessons in programming, learning, and life

ActionScript 3.0 Events: The Myth of useWeakReference

| Comments

Memory management is tricky when working with a garbage collector. Throw in a complex display list hierarchy with lots of event handlers, and it’s definitely a challenge! It’s very easy to overlook some hidden object reference, causing objects to not be marked as garbage. Soon your memory usage is spiraling out of control! This has lead many developers such as Grant Skinner and Ted Patrick to suggest always setting useWeakReference to true when you call addEventListener.

This is indeed a good idea and can be a helpful failsafe, but I think that it fails to emphasize another important action: Always remove your event listeners. I fear that many developers may be given a false sense of security by always using weakly referenced listeners, so I’ve tried to create a clear description of how event listeners create references and how to clean them up. This assumes you have an idea of how Flash’s mark-sweep garbage collector works. If not, check out Grant’s excellent AS3 Resource Management articles, which are a must read for any Flash developer.

Event listeners and references

Let’s look at the creation of a simple event listener:

1
dispatcher.addEventListener(Event.COMPLETE, listener.handlerMethod);

Here we can see the two objects involved in event handling: the dispatcher and the listener. Note that they could refer to the same object. They could also be omitted, in which case they implicitly refer to this (i.e., the object running that line of code).

addEventListener creates a reference from the dispatcher to the listener. The reference is needed to call the handler function when the event fires. This means that listener can not be garbage collected until it is removed from the dispatcher, or until dispatcher is also eligible for garbage collection.

Listeners do not prevent a dispatcher from being garbage collected. References are one way, not bidirectional. If we cleared all references to dispatcher, it would have no references pointing to it. It would be eligible for garbage collection, regardless of whether we removed the listener.

useWeakReference

Let’s look at how useWeakReference affects things. Weak references are not traversed during mark-sweep garbage collection. Therefore, weak references will allow your listener objects to be garbage collected in certain situations where they otherwise would not. Let’s consider the common example of a Player class that would watch for key presses:

1
2
3
4
5
6
7
public class Player extends MovieClip {
  public function initPlayer():void {
      stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
  }

  public function onKeyDown(e:Event):void { /* ... */ }
}

Imagine that our player dies, and we want him to be cleaned up. However, the event listener creates a reference from the stage to the player. The stage is the topmost display object and is always accessible. Therefore, when the mark-sweep process runs, this event listener allows the garbage collector to hop from the stage to our player object, even if we’ve cleared all other references and removed it from the display list. Our player object will never be collected and will sit around wasting memory! But what if we set useWeakReference to true?

1
  stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown, false, 0, true);

Now, despite our listener, the garbage collector can not travel from the stage to our player object. The player will be marked as unreachable and eligible for collection. Weak references helped us out here.

However, let’s consider a different example. Imagine a shooting game where an enemy is killed when clicked by the user:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Game extends MovieClip {
  private var enemy:Enemy;
  public function Game() {
      enemy = new Enemy();
      addChild(enemy);
      enemy.addEventListener(MouseEvent.MOUSE_DOWN, onEnemyClicked, false, 0, true);
  }

  public function onEnemyClicked(event:Event):void
  {
      removeChild(enemy);  // kill the enemy
      enemy = null;
  }
}

In this example, the enemy is the dispatcher, creating a reference from the enemy to the root game object. It doesn’t matter if we use a weak reference here. After our enemy dies, when the garbage collector runs, it can’t hop from the root to the enemy. The reference is in the opposite direction, from enemy to root! Therefore, the enemy doesn’t get marked and will be eligible for garbage collection, even though someone is still listening to it.

useWeakReference only has an effect when the listener object has a shorter lifetime than the dispatcher. Usually this happens when a “child” object is listening for events from a “parent” object. In our first example, the player was a transient child object listening to the long-lived global stage. This is the minority case—more often, an object will listen to events from child objects or itself.

Unfortunately, all of this talk misses an even bigger problem. Regardless of any use of weak references, an object may continue to dispatch and listen to events until it gets garbage collected. Since the garbage collector runs at some indeterminate point in the future (possibly never), this could take quite a while! In our first case, the stage will continue to throw KEY_DOWN events, triggering a response inside the player, even though he’s dead! For events like KEY_DOWN and ENTER_FRAME, this is a source for many subtle bugs and performance problems. We really need to remove our listeners manually!

Clean up after yourself

All of the above pitfalls are avoided if we follow a simple rule: Regardless of whether or not you use weak references, at the end of an object’s lifetime, always remove any listeners that it created. If you clear your listeners when you’re done with them, then you will both prevent your events from firing when an object is “dead”, and you will ensure that listeners are not preventing garbage collection of your objects!

Here’s a good checklist to follow when you are done with an object:

  1. Remove it from the display list.
  2. If it’s a MovieClip, tell it to stop().
  3. Remove any event listeners that the object has created.
  4. Clear any references in parent objects by setting them to null.

Implementing a dispose method on your objects makes it easy to manage your cleanup duties. Here’s some sample code illustrating these concepts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package
{
  import flash.display.MovieClip;
  public class Game extends MovieClip
  {
      public var player:Player;

      public function Game()
      {
          player = new Player();  // creates a reference from Game to Player
          addChild(player);       // creates both a reference from Game to Player and vice versa
          player.initPlayer();   // more references created here (see below)

      }

      // all player die someday!
      public function killPlayer():void
      {
          player.dispose();      // tell player to clear its own references and event listeners
          removeChild(player);    // clear the two references created by the display list
          player = null;     // finally, get rid of the direct variable reference
      }
  }
}

package
{
  import flash.display.MovieClip;
  import flash.events.Event;
  import flash.events.KeyboardEvent;

  public class Player extends MovieClip
  {
      public function initPlayer():void {
          // creates a reference from the stage to player
          stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);

          // creates a self-reference from player to player
          addEventListener(Event.ENTER_FRAME, onEnterFrame);
      }

      private function onKeyDown(event:Event):void    { /* ... */ }
      private function onEnterFrame(event:Event):void { /* ... */ }

      public function dispose():void
      {
          // clean up after ourself!
          stop();
          stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
          removeEventListener(Event.ENTER_FRAME, onEnterFrame);
      }
  }
}

Edit 09/27/2012: Corrected a bug in the dispose() code snippet.

Comments