Saving Memory with BitmapData Tricks
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); } } }
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)); } } }
Spot a bug? Have a question or suggestion? Post a comment!
#1 by henke37 on April 21st, 2014 ·
Huh, I would have expected the compressed version of the data to be included somewhere. After all, as previously shown, the player does keep it around to allow for saving memory by redecoding as needed.
#2 by Benjamin Guihaire on April 21st, 2014 ·
Here one technique to save bitmapData memory:
with a GPU enabled flash application, bitmapData needs to be uploaded to GPU to be rendered…you can save lots of bitmapData memory by calling dispose() after uploading your bitmapData to GPU… once uploaded, you probably don’t need the bitmapData
the only time you need to re-upload a bitmapData to GPU is when the context3D change, which is : almost never on osx and windows with most browser (the only 2 cases is when the computer goes to sleep, or if pepper flash on chrome is used and when your app goes to full screen mode.
So we can ignore the context3d loss case if there is no fullscreen more, or at least, support that rare context3d loss case by reloading your bitmapData from a cache.
And save tons and tons of bitmapData memory this way..
Of course, another way to save bitmapData memory is to not use BitmapData (by using ATF for example)
Ben.