How To Reclaim Hidden Unused Bitmap Memory
As I discovered in the previous articles, loaded bitmaps are stored in memory in two forms: the compressed PNG, JPEG, JPEG-XR, GIF file and the uncompressed RGBA pixels. If you don’t use the pixels, Flash Player will reclaim its memory and then uncompress it if you use the bitmap later on. However, if you do plan to use the bitmap, isn’t the compressed file data just memory overhead? Today’s article will show you how to dump this unused file data and save a bunch of memory.
First, simply changing the loaded BitmapData
is not enough to invalidate Flash’s cached file data. Even though you’ve changed some pixels of your BitmapData
, Flash will still dump your changed pixel data if you don’t use it for a while. Then it’ll uncompress the original image file data when you use the BitmapData
later on. This would seem to be an error or a bug in the Flash Player. If you know more about this process, please feel free to explain in the comments.
What’s needed is to actually create an entirely new BitmapData
. This new object will not be loaded from an image file, either by URL or by a ByteArray
. Instead, it’s simply a clone of the BitmapData
you did load from an image file:
bmd = bmd.clone();
Now that’s a simple trick! Unfortunately, this one-liner is deceptive in its simplicity. The five lowly letters c-l-o-n-e result in a memory allocation large enough to fit all of the pixels of the bitmap, so Flash is temporarily using twice the memory necessary to store the bitmap. Also, Flash needs to copy the memory from one bitmap to another which may take a lot of time if you have a lot of pixels, either in one bitmap or many.
Now, let’s check out a test app to demonstrate the effectiveness of this trick. The test app loads an image file’s bytes and then loads 100 bitmaps from those bytes. Each bitmap is unique and taking up lots of memory. BitmapPreserver
from the last article makes sure that their pixel data isn’t reclaimed by Flash Player. Observe the memory display and then click the button to unload the compressed file data for all of the bitmaps using the above trick. The image I’m using is about 60KB, so you should see a drop of about 6MB when you click that button. A debug version of Flash Player is required so that System.gc()
can be run in order to ensure a timely result when you click the button. Have a look at the source code and then launch the app:
package { import flash.utils.ByteArray; import flash.net.URLLoaderDataFormat; import flash.net.URLLoader; import flash.system.Capabilities; import flash.utils.getTimer; import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.Loader; import flash.display.LoaderInfo; import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import flash.net.URLRequest; import flash.system.System; import flash.text.TextField; import flash.text.TextFieldAutoSize; import flash.text.TextFormat; public class UnloadingCompressedBitmaps extends Sprite { private static const NUM_BITMAPS:int = 100; private var logger:TextField; private var bmdBytes:ByteArray; private var bmds:Vector.<BitmapData>; private var bmdCreateTime:int; public function UnloadingCompressedBitmaps() { logger = new TextField(); logger.autoSize = TextFieldAutoSize.LEFT; addChild(logger); if (!Capabilities.isDebugger) { logger.text = "This app is designed to run in a debug version of Flash Player"; return; } logger.text = "Loading bitmaps..."; bmds = new Vector.<BitmapData>(); var loader:URLLoader = new URLLoader(); loader.dataFormat = URLLoaderDataFormat.BINARY; loader.addEventListener(Event.COMPLETE, onBytesLoaded); loader.load(new URLRequest("Adobe_Flash_Professional_CS5_icon.png")); } private function onBytesLoaded(ev:Event): void { bmdBytes = (ev.target as URLLoader).data; loadBitmap(); } private function loadBitmap(): void { var loader:Loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoaded); loader.loadBytes(bmdBytes); } private function onLoaded(ev:Event): void { var bmd:BitmapData = ((ev.target as LoaderInfo).content as Bitmap).bitmapData; BitmapPreserver.addBitmap(bmd); bmds.push(bmd); if (bmds.length == NUM_BITMAPS) { bmdCreateTime = getTimer(); var tf:TextField = new TextField(); tf.mouseEnabled = false; tf.selectable = false; tf.defaultTextFormat = new TextFormat("_sans"); tf.autoSize = TextFieldAutoSize.LEFT; tf.text = "Unload Compressed Bitmap Memory"; var button:Sprite = new Sprite(); button.buttonMode = true; button.graphics.beginFill(0xF5F5F5); button.graphics.drawRect(0, 0, tf.width+3, tf.height+3); button.graphics.endFill(); button.graphics.lineStyle(1); button.graphics.drawRect(0, 0, tf.width+3, tf.height+3); button.addChild(tf); button.addEventListener(MouseEvent.CLICK, onUnload); button.y = height; addChild(button); addEventListener(Event.ENTER_FRAME, onEnterFrame); } else { loadBitmap(); } } private function onEnterFrame(ev:Event): void { System.gc(); logger.text = "System.totalMemory: " + System.totalMemory + " (" + (getTimer()-bmdCreateTime) + " ms. since bitmap created)"; } private function onUnload(ev:Event): void { removeChild(ev.target as Sprite); BitmapPreserver.removeAllBitmaps(); for (var i:int; i < NUM_BITMAPS; ++i) { bmds[i] = bmds[i].clone(); BitmapPreserver.addBitmap(bmds[i]); } bmdCreateTime = getTimer(); } } }
Launch the test app (in a debug player)
Obviously, there is a downside to using this technique. Flash Player is no longer able to reclaim the memory used to store the pixels of BitmapData
objects that it thinks you’re not using. This is bad if you’re really not actually using the bitmaps and can tolerate the CPU hit when the bitmaps get re-uncompressed from the file data. But if you want maximum control, try out this trick and save a boatload of memory in your bitmap-intensive apps!
Spot a bug? Have a question or comment? Can you explain why Flash reclaims changed BitmapData
pixels? Post a comment!
#1 by Dave on June 19th, 2013 ·
Such an obvious trawling ad. Pitiful.
I found this article while trying to fidn ways of getting our bitmapdata usage down. Using the new scout released the other day, I’ve been able to narrow dow where the bitmapdata is getting from, but unfortunately I already suspected these places and am still in the same boat of trying to remove it.
case in point :
This is from ScaleBitmap, a lib from org.bytearray. The additions to it so far have been changing the original bitmpapdata variable to a bmp, and then initialising its data instead. At the end of the function, after passing the bitmapdata back, which basically goes to the super (Bitmap) for use as a replacement bitmapData, I then set the data and the bitmap to null. Which in a stand alone test i ran, was fine. however this data sticks around like a bad smell, and am dead keen on finding a way of killing it off.
Any ideas ?
#2 by jackson on June 19th, 2013 ·
Thanks for spotting that spam comment. Sometimes they slip through. I’ve deleted it now.
As to your issue, I’m not sure exactly what the problem is. What are you doing that is causing bitmap memory to stick around?
#3 by Dave on June 19th, 2013 ·
my apologies for not using the pre/code tags on the post.