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:

  1. Access it with a function like BitmapData.getPixel, my suggested way to preload it
  2. 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();
		}
	}
}

Launch the test app

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