Combine buttonMode = true and a mouse click to leak memory

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 Josh Tynjala

Josh Tynjala is a frontend developer, open source contributor, bowler hat enthusiast, and karaoke addict. You might be familiar with his project, Feathers UI, an open source user interface library for Starling Framework that is included in the Adobe Gaming SDK.

Discussion

  1. Graham

    Hi there. I just wanted to say thanks for posting this! I was having a hell of a time debugging a memory leak in an Away3D-based game that I’m working on when I stumbled upon your buttonMode theory. I was basically at my wits end thinking that the problem was with Away3D, but when I saw this post I thought I’d try removing the buttonMode references to an in-game menu’s buttons. Low and behold it fixed the problem! The garbage collector was then able to swoop in and clear the Sprite containing the Away3D view from memory. Praise the Lord! 🙂 After some further testing I determined that changing the buttonMode to false just before removing the clip from the stage also fixed the problem. That way I still get the nice little hand cursor. See if it works for you too.

  2. Peter

    Fantastic. I am having exactly the same issues and have spent many frustrated hours with the profiler and tearing at my hair. 2 years on and the bug is still there.

    What happens if you set buttonMode to false just before you remove it?

  3. Vizio

    Hi Josh,

    I thought I had the same problem several months ago (I also avidly commented on the bug you opened and written a blog post 🙂 ), but now I’m pretty sure I was doing stuff wrong. It seems to be fixed in Flash Player 10.1 compiling using the FlexSDK 4.1.

    Anyway do you remember exactly which flash player version and flex SDK version you were using when you found the problem?

    cheers