Last week’s article showed a variety of tricks for saving memory with ByteArray. Today’s article explores some tricks to use with BitmapData to save even more memory.

Let’s start with a baseline so we know what a basically-empty BitmapData looks like:

var oneByOne:BitmapData = new BitmapData(1, 1);
getSize(oneByOne); // 160

Perhaps the biggest find in the ByteArray article was that they support copy-on-write. Does BitmapData support it, too? It seems natural that the clone method could return its BitmapData with just a reference to the original and set it up for copy-on-write. Let’s try that out:

var bmd:BitmapData = new BitmapData(512, 512);
getSize(bmd); // 1048672
 
var copy:BitmapData = bmd.clone();
getSize(copy); // 1048672

Unfortunately, that seems to not be the case with BitmapData.clone. A full copy is made when you call clone and no copy-on-write technique is employed.

Another possibility for saving memory is to create the BitmapData with transparent set to false. After all, opaque bitmaps don’t need an alpha channel and can therefore use only 24 bits per pixel instead of 32. So let’s try that:

var opaque:BitmapData = new BitmapData(bmd.width, bmd.height, false);
getSize(opaque); // 1048672

Well, that didn’t work either. Adobe’s docs don’t say either way, but they do mention a rendering performance improvement. That’s not a memory savings, but at least your performance may improve slightly.

Next let’s try embedding a BitmapData into the SWF. This seems like a natural candidate for copy-on-write since the embedded data can’t be changed.

[Embed(source="flash_logo.png")]
private static const FLASH_LOGO_CLASS:Class;
 
var embed:BitmapData = Bitmap(new FLASH_LOGO_CLASS()).bitmapData;
getSize(embed); // 65632
 
var embedSize:BitmapData = new BitmapData(embed.width, embed.height);
getSize(embedSize); // 65632

Another miss. Let’s give copy-on-write one last try. We’ll create a SWF in Flash Pro that has a BitmapData in its library. Then we’ll load that SWF dynamically at runtime and instantiate the library instance.

[Embed(source="flash_logo.png")]
private static const FLASH_LOGO_CLASS:Class;
 
var loader:Loader = new Loader();
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoaded);
loader.load(new URLRequest("hasbitmap.swf"));
 
private function onLoaded(ev:Event): void
{
	var ad:ApplicationDomain = ev.target.applicationDomain;
	var def:Class = Class(ad.getDefinition("logo_flashplayer"));
	var loaded:BitmapData = BitmapData(new def());
	getSize(loaded); // 102544
 
	var loadedSize:BitmapData = new BitmapData(loaded.width, loaded.height);
	getSize(loadedSize); // 102544
}

Sadly, this is another missed opportunity to employ copy-on-write for a huge memory user.

Now let’s move to some longstanding advice: use the same BitmapData for many Bitmap instances on the Stage. Surely Bitmap should only keep references to BitmapData and not make a copy for itself, right?

The following test app endeavors to find out by adding 100 Bitmap instances to the Stage. Two buttons are provided to switch which BitmapData instances they use. The first uses the same green 128×128 BitmapData for each Bitmap. This is the “shared” mode. The second button uses a different red 128×128 BitmapData for each Bitmap. This is the “don’t share” mode.

package
{
	import flash.display.*;
	import flash.text.*;
	import flash.events.*;
	import flash.system.*;
 
	[SWF(width=640,height=480)]
	public class ShareBitmapDatas extends Sprite
	{
		private var memUsage:TextField = new TextField();
		private var bmd:BitmapData;
		private var bitmaps:Vector.<Bitmap>;
 
		public function ShareBitmapDatas()
		{
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
 
			if (!Capabilities.isDebugger)
			{
				memUsage.autoSize = TextFieldAutoSize.LEFT;
				memUsage.text = "Debug version of Flash Player required";
				addChild(memUsage);
				return;
			}
 
			makeButton("Share BitmapData", onShare);
			makeButton("Don't Share BitmapData", onDontShare);
 
			memUsage.autoSize = TextFieldAutoSize.LEFT;
			memUsage.y = this.height + 5;
			addChild(memUsage);
 
			bmd = new BitmapData(128, 128, false, 0xff00ff00);
 
			bitmaps = new Vector.<Bitmap>(100);
			for (var i:uint = 0; i < bitmaps.length; ++i)
			{
				bitmaps[i] = new Bitmap(bmd);
				var bm:Bitmap = bitmaps[i];
				bm.x = bm.y = 50 + i*3;
				addChild(bm);
			}
 
			addEventListener(Event.ENTER_FRAME, onEnterFrame);
		}
 
		private function onEnterFrame(ev:Event): void
		{
			System.gc();
			var kb:Number = System.totalMemory / 1024;
			var share:Boolean = bitmaps[0].bitmapData == bitmaps[1].bitmapData;
			memUsage.text = "Memory Usage: " + kb + " KB. Sharing? " + share;
		}
 
		private function onShare(ev:Event): void
		{
			test(true);
		}
 
		private function onDontShare(ev:Event): void
		{
			test(false);
		}
 
		private function test(share:Boolean): void
		{
			for each (var bm:Bitmap in bitmaps)
			{
				bm.bitmapData = share
					? bmd
					: new BitmapData(bmd.width, bmd.height, false, 0xffff0000);
			}
		}
 
		private function makeButton(label:String, callback:Function): void
		{
			const PAD:Number = 3;
 
			var tf:TextField = new TextField();			
			tf.name = "label";
			tf.text = label;
			tf.autoSize = TextFieldAutoSize.LEFT;
			tf.selectable = false;
			tf.x = tf.y = PAD;
 
			var button:Sprite = new Sprite();
			button.name = label;
			button.graphics.beginFill(0xcccccc);
			button.graphics.drawRect(0, 0, tf.width+PAD*2, tf.height+PAD*2);
			button.graphics.endFill();
			button.graphics.lineStyle(1, 0x000000);
			button.graphics.drawRect(0, 0, tf.width+PAD*2, tf.height+PAD*2);
			button.addChild(tf);
			button.addEventListener(MouseEvent.CLICK, callback);
 
			button.x = PAD + this.width;
			button.y = PAD;
			addChild(button);
		}
	}
}

Run the test app

With the debug standalone version of Flash Player 13.0.0.182 on Mac OS X 10.9.2 I’m seeing about 11896 KB used when sharing and 18780 KB used when not sharing. This is just about spot on for the extra memory we’d expect to be used by an additional 100 128×128 BitmapData instances.

At least the longstanding advice to have multiple Bitmap instances share the same BitmapData holds. For example, if you’re making a tile-based game you can save a ton of memory by having all the tile Bitmap instances that use the same image have the same BitmapData reference rather than a unique copy per-tile.

In conclusion, BitmapData seems to have no support for copy-on-write unlike the support that ByteArray enjoys. However, you can still save a bunch of memory by smart utilization of Bitmap to share BitmapData instances between them.

Here’s the full source of the size testing app:

package
{
	import flash.display.*;
	import flash.utils.*;
	import flash.text.*;
	import flash.sampler.*;
	import flash.events.*;
	import flash.net.*;
	import flash.system.*;
 
	public class BitmapSize extends Sprite
	{
		[Embed(source="flash_logo.png")]
		private static const FLASH_LOGO_CLASS:Class;
 
		private var logger:TextField = new TextField();
		private function row(...cols): void
		{
			logger.appendText(cols.join(",") + "\n");
		}
 
		public function BitmapSize()
		{
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
 
			logger.autoSize = TextFieldAutoSize.LEFT;
			addChild(logger);
 
			if (!Capabilities.isDebugger)
			{
				row("Debug version of Flash Player required");
				return;
			}
 
			var loader:Loader = new Loader();
			loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoaded);
			loader.load(new URLRequest("hasbitmap.swf"));
		}
 
		private function onLoaded(ev:Event): void
		{
			row("BitmapData", "Size");
 
			var oneByOne:BitmapData = new BitmapData(1, 1);
			row("1x1", getSize(oneByOne));
 
			var bmd:BitmapData = new BitmapData(512, 512);
			row("512x512", getSize(bmd));
 
			var copy:BitmapData = bmd.clone();
			row("Clone of 512x512", getSize(copy));
 
			var opaque:BitmapData = new BitmapData(bmd.width, bmd.height, false);
			row("Opaque 512x512", getSize(opaque));
 
			var embed:BitmapData = Bitmap(new FLASH_LOGO_CLASS()).bitmapData;
			row("Embedded Instance", getSize(embed));
 
			var embedSize:BitmapData = new BitmapData(embed.width, embed.height);
			row("Same Dimensions as Embedded Instance", getSize(embedSize));
 
			var ad:ApplicationDomain = ev.target.applicationDomain;
			var def:Class = Class(ad.getDefinition("logo_flashplayer"));
			var loaded:BitmapData = BitmapData(new def());
			row("Loaded", getSize(loaded));
 
			var loadedSize:BitmapData = new BitmapData(loaded.width, loaded.height);
			row("Same Dimensions as Loaded", getSize(loadedSize));
		}
	}
}

Run the test app

Spot a bug? Have a question or suggestion? Post a comment!