Keeping Bitmaps Decompressed
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 ·
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 ·
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 ·
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 ·
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 ·
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
BitmapData
class is holding the compressed (e.g. PNG, JPEG) data as well as the uncompressed (RGBA) data. If you don’t use theBitmapData
for 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 theBitmapData
again 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 ·
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 ·
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
BitmapPreserver
should preserve yourImageDecodingPolicy.ON_LOAD
-loaded images, so you can mix and match.#8 by Tim on January 22nd, 2013 ·
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 ·
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 ·
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 ·
Sounds like a good test. If I had to guess I would say that uploading the
BitmapData
would cause it to be decompressed and then the GPU would hold it until there is a context loss. TheBitmapData
would 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 ·
Thanks for sharing! Saved me a lot of time!
#13 by Aaron on August 24th, 2013 ·
Thanks for this Jackson. I ran my swf in Adobe Scout CC and was wondering why I was getting large peaks in the “Display List Rendering -> Decompressing Images” as well as a drop in frame-rate every time I re-added my Bitmap to the stage. Preserving the bitmap worked like a charm and all the peaks dropped out to normal levels in Scout, and frame rate went up. Your method works as well if you are dynamically loading BitmapData’s from the internal library:
Would just recommend caution to developers using this method with a lot of large bitmaps on mobile devices. I’m running a slideshow app with around 23 1920×1200 images on a tablet. If I ran preservation on all my bitmap datas, the app would freeze and some bitmaps would go black, so now I’m working on a sort of intelligent/on-demand preservation algorithm.
Wonder if there’s a way to force the freeing decompressed bitmap memory on demand after we preserve it with getPixel()?
#14 by jackson on August 25th, 2013 ·
Glad to hear this has been useful for you. Since you mention mobile, good old System.gc is available under AIR to force the garbage collection process. Of course you’ll need to release all references to the memory first, such as by removing it from the stage and re-assigning all your variables pointing to the
Bitmap
and/orBitmapData
.#15 by Fygo on January 31st, 2014 ·
Very useful (as most of the things on your blog)! I need to keep over 100 HD images in memory and until now they always got released. Excellent post, helped me out a lot.
#16 by Fygo on February 18th, 2014 ·
I gotta say I ran into a rather unusual problem, I will just type it here so maybe it will help somebody. The above code works flawlessly for a few smaller images but it completely fails on me on Windows with approx. 50 HD images. (test movie unloaded, published air app just hangs for ever). Tested on 3 Windows machines (2x Win 7 64 bit, 1x Windows 8). It seemed like there is some problem while looping thru so many big images so I better created an enter frame event and was taking one bitmap per frame. Again no go, very same result. Then I added a canvas bitmap and instead of getPixel(0, 0) I actually set the bitmapdata of the canvas to the “processed” bitmapdata. This canvas needs to be added to stage (I mean, it needs to be “visible”). Voila, it works now without hanging the app. This happens only and only on Windows. On Mac it worked flawlessly since the beginning. Hm. I really don’t get Flash sometimes.
#17 by jackson on February 18th, 2014 ·
All platforms will have bugs and this sounds like one of them given that it works on Mac. Have you checked out Adobe’s bugbase? If you don’t find anything, you might want to write up a bug report.
#18 by Addicting Games on June 13th, 2014 ·
The increasing attractiveness of Nintendo Wii is storming the gaming planet with just about
every moment that goes by. From athletics to approach game titles
to puzzles, you can obtain any recreation you like, appreciate hrs of enjoyment and come to be aspect of the massive international multiplayer on-line gaming group.
This can be anything from pulling weeds to watering the bouquets.
#19 by Leogoldseed on October 9th, 2014 ·
For my app, I tried your solution, this one also: http://help.adobe.com/en_US/as3/dev/WS52621785137562065a8e668112d98c8c4df-8000.html
But nothing really helps in any significant way. Embedding the bitmaps in the swf as lossless / no compression helped a lot, but it’s still jittery sometimes…
Interestingly enough, instead of having several tiles in my game, I combined them all together in one big PNG, and that performed better than having several tiles.
It’s so frustrating. I really wish there was a better alternative to this decompression surges shown by Scout.
Have tried anything new with better results?
Thanks,
Leo.
#20 by jackson on October 9th, 2014 ·
I haven’t tried anything new due to the switch to Unity, but thanks for posting about the lossless setting being helpful.
#21 by Eric Holmes on March 5th, 2015 ·
I used this methodology for an app I just built, and it truly is the only solution out there – I tried 6 other approaches and none worked.
One addition I did due to the sheer number of images I needed to “ping” was reduced the timer time, and instead of doing all images at once, I cycled through the array using an index. That way I spread out the ~100ms time (20 images) to 5 ms every quarter second, keeping the spikes out of the application. Worked great!