Combine buttonMode = true and a mouse click to leak memory

by Josh Tynjala

Here’s a troublesome Flash Player bug I discovered when I couldn’t understand why my SWF wouldn’t unload properly from a parent SWF. Everything seemed to unload fine unless I clicked on any buttons in my game’s menus. How strange. I loaded my SWF up in the Flex Builder profiler and watched what stuck around. Sure enough, in addition to the main document class and anything referenced there, some button instances that should be gone seemed to be sticking around. The reason they were stuck in memory? I set buttonMode to true.

Sounds weird, right? At first, following my own recent advice on how to cleanup an unloading SWF, I started commenting out certain mouse events used by the buttons. My thinking was that maybe these event listeners were causing the stage to keep a reference to the button. Unfortunately, that didn’t seem to help. In frustration, I started commenting out large portions of the button class. With almost no code left to run, I got the SWF to unload properly. No instances of any class in my SWF were left this time, so it was clear that this button class was the culprit. I began slowly adding bits of functionality back in. I eventually discovered two functions that would cause the button to stay in memory, and both had one thing in common: they modified buttonMode. I re-enabled all my code in that class, except for the lines that set buttonMode, and the SWF began unloading perfectly.

That’s a pretty complex case that involves multiple SWFs, and it certainly wasn’t easy to debug. For my bug report to Adobe, I built a very simple test case that you can look at below. I simply creates two Sprite instances that are clickable, one with buttonMode set to true, and one without:

package
{
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	import flash.system.System;

	public class ButtonModeBug extends Sprite
	{
		public function ButtonModeBug()
		{
			var button1:Sprite = new Sprite();
			button1.graphics.beginFill(0x009900);
			button1.graphics.drawRect(0, 0, 120, 20);
			button1.graphics.endFill();
			button1.x = 10;
			button1.y = 50;
			button1.addEventListener(MouseEvent.CLICK, regularSpriteClickHandler);
			this.addChild(button1);

			var button2:Sprite = new Sprite();
			button2.graphics.beginFill(0x990000);
			button2.graphics.drawRect(0, 0, 120, 20);
			button2.graphics.endFill();
			button2.x = 150;
			button2.y = 50;
			button2.buttonMode = true;
			button2.addEventListener(MouseEvent.CLICK, buttonModeSpriteClickHandler);
			this.addChild(button2);
		}

		private function regularSpriteClickHandler(event:MouseEvent):void
		{
			var button1:Sprite = Sprite(event.currentTarget);
			button1.removeEventListener(MouseEvent.CLICK, regularSpriteClickHandler);
			this.removeChild(button1);
			button1 = null;
			System.gc();
		}

		private function buttonModeSpriteClickHandler(event:MouseEvent):void
		{
			var button2:Sprite = Sprite(event.currentTarget);
			button2.removeEventListener(MouseEvent.CLICK, buttonModeSpriteClickHandler);
			this.removeChild(button2);
			button2 = null;
			System.gc();
		}
	}
}

Notice that when you click either button, it is removed from the display list, the event handler is removed, the variable is set to null, and then System.gc() is called to force the garbage collector to run.

To see the bug in action, you should run that class in the profiler with live memory data displayed (you may need to unfilter the flash.* classes too). When you click button1 (the green one), you can see the number of Sprite instances decrease by one. When you click button2 (the red one), the number of Sprite instances stays the same.

As a control case, comment out the line that says button2.buttonMode = true;, and run again. Both buttons will be removed from memory, and the number of Sprite instances will decrease to zero.

The description of this bug can be summarized like this:

If you set buttonMode to true on a Sprite, and that Sprite gets clicked by the user, it will not be removed from memory after all references have been removed in ActionScript.

It appears, although I cannot be certain, that Flash Player’s internal code isn’t properly releasing its own reference to the Sprite. Maybe related to the hand cursor? Whatever the case may be, I recommend being wary of buttonMode (and the related useHandCursor) until this gets fixed, especially if you have a SWFs that are loaded and unloaded inside other SWFs. I say that because your buttons will hold many more objects in memory when the SWF can’t unload properly. If you’ve encountered the same bug, please vote for it in my report, “buttonMode = true + mouse click keeps Sprite in memory” filed in the public Adobe bug system.

About the Author

Josh Tynjala is an indie game developer, entrepreneur, Flash and Flex mercenary, and bowler hat enthusiast.

Discussion
  1. Shouldn’t you use weak reference?

    button1.addEventListener(MouseEvent.CLICK, regularSpriteClickHandler,false,0,true);

    useWeakReference:Boolean (default = false) — Determines whether the reference to the listener is strong or weak. A strong reference (the default) prevents your listener from being garbage-collected. A weak reference does not.

    Regards,

    posted by markval on 05.01.2009
  2. Markval, the event listeners have no effect on whether the buttons are garbage collected. Remember, the event dispatcher (the button) holds a reference to the listener (the function on my document class). It’s not the other way around.If nothing holds a reference to an event dispatcher, but the dispatcher has any number of listeners, the dispatcher will still be garbage collected.

    You can see this fact explained in the very description you posted (emphasis mine):

    A strong reference (the default) prevents your listener from being garbage-collected.

    Finally, let me point out that setting useWeakReference doesn’t matter anyway. I’m explicitly removing the event listeners.

    If you run the code in the profiler, you’ll see that the buttonMode property is the sole reason why button2 is staying in memory. Comment out the line where that property gets set, run the code again, and button2 will get garbage collected, just like button1.

    posted by Josh Tynjala on 05.01.2009
  3. what happens when set buttonMode back to false before clean loaded SWFs ?

    posted by dim on 05.02.2009
  4. dim, I tried that. If a button has been clicked that had buttonMode set to true, it will not help to set it to false later.

    posted by Josh Tynjala on 05.02.2009
  5. Hey — Does this problem happen with movie clips as well?

    posted by Ryan on 05.07.2009
  6. Ryan, I didn’t test MovieClip, but since it’s a subclass of Sprite, I’d expect it to behave exactly the same.

    posted by Josh Tynjala on 05.07.2009
  7. Funny I forgot about it subclassing the Sprite class. Great article though.

    posted by Ryan on 05.07.2009
  8. I never seen this line before:

    System.gc();

    Are you calling the Garbage Collector?

    posted by Thomas on 06.14.2009
  9. Thomas, yes, it calls the garbage collector. Documentation: System.gc(). It’s meant to be used with the debugger version of Flash Player. I don’t think it has any effect in the regular Flash Player that non-developers install.

    posted by Josh Tynjala on 06.14.2009
  10. Josh,
    you’ve just saved my day!!!!
    I spent the last few days battling against this bug, I couldn’t understand what I was doing wrong , I cleared all the references etc. but nothing the external SWF would not unload!!!
    Then after reading your post and setting buttonMode = false, everything worked fine.

    I commented on your bug report on JIRA and so did @alecmce and I’m surprised they set it as “Not a bug”….. we need some people at #Adobe that listen to us developers!!!!!!! :)

    posted by Simone on 11.16.2009
  11. [...] Combine buttonMode = true and a mouse click to leak memory [...]

    posted by vizio 360º » buttonMode=true prevent swf to unload on 11.30.2009
  12. Hey Josh,

    Something about this post made me think it was related to a similar bug I found.

    In essense, stage.focus retains a reference to the button when it is removed. Just added trace(stage.focus); to the end of each click handler in your example, and it would appear that this is one of the the differences between buttonMode=true/false.

    Unfortunately I don’t have Flash Pro at work, so I won’t be able to test this in the profiler until I get home tonight.

    posted by Rob on 11.30.2009
Share Your Thoughts

To display code in comments: <pre>Code here. May be multiline. Format XML with &gt; and &lt; entities.</pre>

Some HTML allowed in comments: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>