It came to my attention in the comments of Preloading Bitmap Decompression that Flash Player would actually free the decompressed bitmap memory if you didn’t make active use of it, similar to garbage collection. So if you followed my strategy from that article to preload a bitmap, it may have been un-preloaded for you by Flash Player! Today’s article shows you how to work around this little problem.
Flash Player will free the decompressed bitmap memory of your BitmapData if you don’t make use of it in one of two ways:
- Access it with a function like
BitmapData.getPixel, my suggested way to preload it - Add it to the stage for normal rendering
So the workaround is simple- keep accessing it with BitmapData.getPixel. Of course we don’t want to incur a big performance cost just to keep the bitmap preloaded, so we just access it periodically. Experimental testing has shown that Flash Player reclaims the bitmap memory about every 10 seconds, so accessing it with BitmapData.getPixel every second should be good enough.
Here’s a little utility class that will handle the work for you: BitmapPreserver. Simply call BitmapPreserver.addBitmap and it’ll keep periodically accessing it every second. And don’t worry that BitmapPreserver will keep your BitmapData from being garbage collected; it’s held with a weak reference. Also, you can explicitly remove any BitmapData or all of them. You can also start and stop the periodic access for maximum control. Here’s the code:
package { import flash.events.TimerEvent; import flash.utils.Timer; import flash.display.BitmapData; import flash.utils.Dictionary; /** * Holds BitmapData objects in order to keep them preloaded (i.e. their contents will not be * garbage collected) * @author Jackson Dunstan, JacksonDunstan.com/articles/2105 */ public class BitmapPreserver { private static var bitmaps:Dictionary = new Dictionary(true); private static var timer:Timer = new Timer(1000); { timer.addEventListener(TimerEvent.TIMER, onTimer); } private static function onTimer(ev:TimerEvent): void { for (var bmd:* in bitmaps) { bmd.getPixel(0, 0); } } public static function addBitmap(bmd:BitmapData): void { bitmaps[bmd] = true; } public static function removeBitmap(bmd:BitmapData): void { delete bitmaps[bmd]; } public static function removeAllBitmaps(bmd:BitmapData): void { bitmaps = new Dictionary(true); } public static function startPreserving(): void { timer.start(); } public static function stopPreserving(): void { timer.stop(); } } } |
And here’s a little test app to show that first demonstrates the problem by not placing the BitmapData in BitmapPreserver, waiting for the bitmap memory to be reclaimed, and then trying again by actually using BitmapPreserver. Wait about 10 seconds and you’ll see the bitmap memory gets collected, at least on Flash Player 11.5.31.137 on Mac OS X 10.8. After you click the button to try again with BitmapPreserver, it should just keep running forever without letting the bitmap memory get reclaimed by Flash Player.
package { 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; import flash.utils.getTimer; public class KeepingBitmapsPreloaded extends Sprite { private var logger:TextField; private var bmd:BitmapData; private var lastMemory:uint; private var lastMemoryTime:int; private var keepPreloaded:Boolean; public function KeepingBitmapsPreloaded() { logger = new TextField(); logger.autoSize = TextFieldAutoSize.LEFT; addChild(logger); init(); } private function init(): void { removeChildren(1); var loader:Loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoaded); loader.load(new URLRequest("Adobe_Flash_Professional_CS5_icon.png")); } private function onLoaded(ev:Event): void { bmd = ((ev.target as LoaderInfo).content as Bitmap).bitmapData; bmd.getPixel(0, 0); if (keepPreloaded) { BitmapPreserver.addBitmap(bmd); } lastMemory = System.totalMemory; lastMemoryTime = getTimer(); addEventListener(Event.ENTER_FRAME, onEnterFrame); } private function onEnterFrame(ev:Event): void { var curMemory:int = System.totalMemory; var curTime:int = getTimer(); const oneMegabyte:uint = 1024*1024; if (curMemory < lastMemory-oneMegabyte) { logger.text = "Memory dropped " + (lastMemory-curMemory) + " bytes from " + lastMemory + " to " + curMemory + " in " + (curTime-lastMemoryTime) + " ms."; removeEventListener(Event.ENTER_FRAME, onEnterFrame); var tf:TextField = new TextField(); tf.mouseEnabled = false; tf.selectable = false; tf.defaultTextFormat = new TextFormat("_sans"); tf.autoSize = TextFieldAutoSize.LEFT; tf.text = "Restart And Keep Preloaded"; 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, onRestart); button.x = (stage.stageWidth - button.width) / 2; button.y = (stage.stageHeight - button.height) / 2; addChild(button); } else { logger.text = "System.totalMemory: " + curMemory + " (substantially unchanged for " + (curTime-lastMemoryTime) + " ms.)"; } } private function onRestart(ev:Event): void { keepPreloaded = true; init(); } } } |
Spot a bug? Have a question, comment, or suggestion? Post a comment!

#1 by henke37 on January 21st, 2013 · | Quote
Or you could just make the reloading illegal by changing the actual pixel data. It would be a grave error to reload a changed BitmapData.
#2 by jackson on January 21st, 2013 · | Quote
I tried doing that but the uncompressed bitmap memory is still freed. If you want to try it out, add this line after the bitmap is loaded in the test app above:
Even changing it to another value doesn’t seem to stop Flash Player from freeing the memory:
Perhaps they are committing a “grave error”…
#3 by AlexG on January 21st, 2013 · | Quote
I am not getting the point why we should protect BitmapDatas from garbage collector, because garbage collector doesnt collect the objects which have at least on reference in the code. So its enough to have a object pointing to the BitmapData you want to keep alive. Did I miss something except the time spent for decompressing the BitmapData?
#4 by Deril on January 21st, 2013 · | Quote
This might be good optimization in certain conditions:
1 – you have large asset that is rarely used. (rarer then 10 sec)
2 – but THEN it is used.. decompressing image is too expensive.
Imagine fast action game… and explosion happens! machine already struggling hard to show it all because of large explosion.. and then you add decompressing!
Cost of decompression rises then you are doing on less powerful mobile devices.
—–
In any case.. I hate idea having getPixel() call every second… maybe it is possible to trick flash into thinking that asset is used… (push it in display list somehow… maybe just 1 pixel of it…) but keep it out of render loop so it will not hurt render performance at all… maybe just keep that pixel out of screen will do the trick.) Have to test it.
#5 by jackson on January 21st, 2013 · | Quote
That’s a good usage example. Another would be a preloading process. Say your game has a bunch of bitmaps that the first level uses and you load them all but don’t actually use them (e.g. put them on the display list, upload them as textures) during a loading screen. When you clear the loading screen and start the first level you’re going to suddenly have a huge CPU hit as Flash Player decompresses all of the bitmaps on the first frame of the game. This may cause a huge sag in the FPS.
As for what’s actually happening, the
BitmapDataclass is holding the compressed (e.g. PNG, JPEG) data as well as the uncompressed (RGBA) data. If you don’t use theBitmapDatafor a while (e.g. 10 seconds), Flash Player is dumping the uncompressed (RGBA) data to save memory while keeping the compressed (e.g. PNG, JPEG) data in case you want to use theBitmapDataagain later. If you do, Flash Player will uncompress the (PNG, JPEG) data again (to RGBA), which may take up a lot of CPU and case one of the problems like Deril or I describe.#6 by MikeA on January 23rd, 2013 · | Quote
You could also try setting the loader context to decode the image at load time instead of doing on on demand when it shows up on the display list. This should signal the Flash Player to not dump the decompressed version of the image if you haven’t drawn it on the display list recently.
Here’s Thibault’s post from bytearray.org when it was added to AIR 2.6:
http://www.bytearray.org/?p=2931
And the article in Adobe’s help docs for FP11:
help.adobe.com/en_US/as3/dev/WS52621785137562065a8e668112d98c8c4df-8000.html
#7 by jackson on January 23rd, 2013 · | Quote
That’s a good idea for a lot of use cases (e.g. loading huge images), but I tried it out and the uncompressed bitmap memory still gets reclaimed so it’s not quite a fit for this purpose. Still, the
BitmapPreservershould preserve yourImageDecodingPolicy.ON_LOAD-loaded images, so you can mix and match.#8 by Tim on January 22nd, 2013 · | Quote
Alternately, you could launder the bitmapData by copying its pixels into a new BitmapData instance and then leaving the original asset to the garbage collector.
You’d take a relatively small one-time CPU hit from copying the data, and you’d briefly use extra memory to hold two copies of the decompressed bitmap. BUT, once you’re done, the compressed data can also be released.
#9 by jackson on January 22nd, 2013 · | Quote
That sounds like an interesting alternative. The one-time hit to CPU and memory is a bit rough, but the long-term payoff in dumping the compressed data is intriguing.
#10 by NoriSte on February 14th, 2013 · | Quote
Very interesting! Thank you to sharing it! I’ll do some tests with Starling/Stage3D to understand if the regular re-decompression affect it.
I don’t think it’s the same considerations are true because in that case the images are stored both into the main memory and into the GPU memory but the first ones should never be reused… but at the opposite I can tell you that the GPU memory is far and far less capable than the main one (RAM) so it could be also possible that the re-decompression could be emphasized because of the less-capable memory of the graphic card…. I’ll update you :)
#11 by jackson on February 14th, 2013 · | Quote
Sounds like a good test. If I had to guess I would say that uploading the
BitmapDatawould cause it to be decompressed and then the GPU would hold it until there is a context loss. TheBitmapDatawould then have its decompressed memory (i.e. the pixels) reclaimed if it wasn’t used again for a while, including not being uploaded to aTexture. Let’s see if your results match my prediction! :)#12 by Raphael Santos on May 10th, 2013 · | Quote
Thanks for sharing! Saved me a lot of time!