ImageCache, a cheap way to cache images in Adobe Flex

In a previous post about Ely’s SuperImage, I mentioned that we decided to implement a simpler method for caching images within a Flex application. ImageCache is a simple class file that extends the Image control by adding the ability for the control to cache bitmap data. Unlike SuperImage, ImacheCache can handled SWF files, it just lacks all the bells and whistles of SuperImage, though image flicker is eliminated.

Using ImageCache

ImacheCache is just two class files. ImageCache.as which extends the Image control and ImageCachUtility.as which is a Singleton class that used by Imagecache to store and retrieve the BitmapData for cached images. Bitmap data is stored in a regular ArrayCollection object and retrieved by using the full path to the image (because image names can be duplicates but paths should be unique). Once the amount of cached images reaches the cache limit, the controls works on first in first out, dropping off older cached images as it caches new ones and staying within the cache limit. SWF fiels are not actually cached and will load as normal. You just use ImageCache like you would the Image control within your project:


<controls:ImageCache cacheLimit="200" id="image" source="{data.url}" width="100" height="100" complete="imageComplete()" ioError="imageError()" xmlns:controls="com.thanksmister.controls.*"/>

Cache Limit

ImageCache has a property called “cacheLimit” which tells the control how many images to cache. Because caching images can reduce the performance of your application. The larger the cache limit value, the more memory your application will use because it is storing all the Bitmap data in memory. If you want to reset the image cache, create an instance of the ImageCacheUtility and call the method “clear()”. ImageCache has been used and tested extensively for large media sites. Below is a screen shot of images from Flickr, the list on the left uses the standard Image control, the list on the right uses the ImageCache control. It seems that there may be some security issue when loading Flickr images without doing a proxying the images. Run the example locally or proxy Flickr images for best results.

Example

Code

You can download the code for the above project here. You will need your own Flickr API key to make the application work.

-Mister

30 Comments

  1. Thank your for this, its just what I was looking for. I am having an issue though. My application is using a large TileList, 20+ visible tiles at a time, and is loading images 3mb – 5mb or larger. I am getting an error “Invalid Bitmap Data” on this line –

    var bd : BitmapData = new BitmapData( target.contentWidth, target.contentHeight)

    inside the getBitmapData function. Any ideas on a cause or remedy for this? Thanks,
    Scott.

  2. Thank you for this useful ImageCache component!!!

    There are two things need to be noticed:
    1. This fantastic ImageCache can also cache swf files! However, only static swf file (actually the first frame??) can be cached correctly, because it is cached as bitmap data;

    2. It does not dispatch COMPLETE event when the cached image is loaded again, for people who need the COMPLETE event to drive your program could add a line “dispatchEvent(new Event(Event.COMPLETE));” into the “public override function set source(value:Object):void” in the ImageCache.as. And this trick works well so far.

    1. @Jia, Thanks for your observation about SWF caching, that part is true, it will not cache SWF animations. However, you can change the class file to not cache SWF files at all, so they load properly. I like your enhancement for the Event.Complete, thanks for the input!

  3. I’m using ImageCache and pulling images from a remote site to a TileList in my Flex app. Problem is, I’m getting the security violation you mentioned. Is there any way to modify the code to bypass this? No ImageCache means no security issues, but it would be nice to cache.

    Otherwise, it’s modular, easy to use, well commented, a real slam dunk. Thanks for the contribution.

    -j

    1. You can try to proxy your images through a server-side call or use ImageCache in an AIR application to avoid security issues. If ImageCache doesn’t do the trick, try SuperImage by Ely Greenfield on his blog. I also posted a port of SuperImage here https://thanksmister.com/?p=265.

  4. Nice stuff, however, I got white background when I use transparent PNG, do you have any idea to fix it?

    Cheers

  5. Nice work. I modified your Utility class, so that it also caches loading the same image multiple times:

    [as]
    /*
    ImageCache
    version 1.1.0
    Created by Michael Ritchie (Mister)
    modified for loadCaching JPC
    mister@thanksmister.com
    http://www.thanksmister.com

    Simple component to cache Images loaded from URLs. This could
    easily be expanded to cache any type of information loaded into
    the Image control. For more information, check Ely Greenfields post:

    http://www.quietlyscheming.com/blog/2007/01/29/new-flex-componentsample-superimage/

    This is release under a Creative Commons license. More information can be
    found here:

    http://creativecommons.org/licenses/by/2.5/
    */

    package com.thanksmister.caching
    {
    import flash.display.Bitmap;
    import flash.display.BitmapData;
    import flash.geom.Matrix;

    import mx.collections.ArrayCollection;
    import mx.controls.Image;

    public class ImageCacheUtility
    {
    private static var imageCache : com.thanksmister.caching.ImageCacheUtility;

    private var imageDictionary:ArrayCollection = new ArrayCollection();
    private var _cacheLimit:Number = 50;

    public function set cacheLimit(num:Number):void
    {
    _cacheLimit = num;
    }

    public function get cacheLimit():Number
    {
    return _cacheLimit;
    }

    public function ImageCacheUtility()
    {
    if ( com.thanksmister.caching.ImageCacheUtility.imageCache != null )
    throw new Error( “Only one instance should be instantiated” );
    }

    public static function getInstance() : com.thanksmister.caching.ImageCacheUtility
    {
    if ( imageCache == null )
    imageCache = new com.thanksmister.caching.ImageCacheUtility();

    return imageCache;
    }

    public function cacheImage(id:String, source:ImageCache):void
    {
    var obj:Object = new Object();

    for each ( var newObj:Object in imageDictionary)
    {
    if(newObj.id == id)
    if (newObj.data != null)
    return;
    else
    obj = newObj;
    }

    var bd : BitmapData = getBitmapData( source );

    obj.data = bd;
    imageDictionary.addItem(obj);

    for each (var image:ImageCache in obj.others)
    image.source = obj.id;

    checkLimit();
    }

    public function loadImage(id:String,source:Image):*
    {
    var newObj:Object = new Object();

    var bm:Bitmap = new Bitmap();
    for each ( var obj:Object in imageDictionary){
    if(obj.id == id) {
    if (obj.data != null){
    bm = new Bitmap( obj.data );
    return bm;
    }
    else
    (newObj = obj);
    }
    }

    if (newObj.id == null) {
    newObj.id = id;
    newObj.others = new ArrayCollection();
    imageDictionary.addItem(newObj);
    return id;
    }
    else{
    (newObj.others as ArrayCollection).addItem(source);
    return null;
    }
    }

    private function getBitmapData( target : Image ) : BitmapData
    {
    var bd : BitmapData = new BitmapData( target.contentWidth, target.contentHeight);
    var m : Matrix = new Matrix();
    bd.draw( target, m );
    return bd;
    }

    public function clear():void
    {
    imageDictionary.removeAll();
    }

    public function removeImage(id:String):void
    {
    var i:Number = 0;
    for each ( var obj:Object in imageDictionary){
    if(obj.id == id) {
    imageDictionary.removeItemAt(i);
    return;
    }
    i++
    }
    }

    private function checkLimit():void
    {
    var i:Number = 0;
    while(imageDictionary.length > _cacheLimit)
    {
    imageDictionary.removeItemAt(i);
    i++;
    }
    }
    }
    }

    [/as]

    1. I am looking at your modification, but still not certain how this changes the default behavior. The objects you are creating are not stored any place and are thrown away, so how are the referenced again in cacheImage and loadImage functions. For example you have this in loadImage:

      [as]
      (newObj.others as ArrayCollection).addItem(source);
      return null;
      [/as]

      You are adding the image source to newObj.others, then returning null. newObj is a variable only scoped to loadImage, so the data is not preserved any place. Maybe you can explain how this would work?

  6. Just few minor notes:

    1. I’m looking at your code and I wonder why do you pass cacheLimit via ImageCache? I think Image shouldn’t know about any limits, this is job of yours singleton.

    2. It’s better to refactor singleton to use internal class inside.

  7. Great work! Right now looking to see if any additions can be integrated with you script, so if images return a 404 error you can set a default image or behaviour (Flex’s broken image icon is ugly). If anyone already has a link, let me know! Thanks.

  8. Nice work!

    But there is an issue with transparent images. Transparency works only at first loading. When display same image again, transparent areas will become white…

  9. Hello again!
    And sorry for my bad english, its not my native-language 😉
    I fixed the issue with images with transparent background. There are also some other things/optimizations i did…

    ImageCacheUtility.as:
    [as]
    /*
    ImageCache
    version 1.0.0
    Created by Michael Ritchie (Mister)
    mister@thanksmister.com
    http://www.thanksmister.com

    Simple component to cache Images loaded from URLs. This could
    easily be expanded to cache any type of information loaded into
    the Image control. For more information, check Ely Greenfields post:

    http://www.quietlyscheming.com/blog/2007/01/29/new-flex-componentsample-superimage/

    This is release under a Creative Commons license. More information can be
    found here:

    http://creativecommons.org/licenses/by/2.5/
    */

    package com.thanksmister.caching
    {
    import com.thanksmister.events.ImageCacheEvent;

    import flash.display.Bitmap;
    import flash.display.BitmapData;
    import flash.events.EventDispatcher;

    import mx.collections.ArrayCollection;
    import mx.controls.Image;

    public class ImageCacheUtility extends EventDispatcher
    {
    private static var imageCache:com.thanksmister.caching.ImageCacheUtility;

    private var imageDictionary:ArrayCollection = new ArrayCollection();
    private var _cacheLimit:Number = 100;

    public function set cacheLimit(num:Number):void
    {
    _cacheLimit = num;
    }

    public function get cacheLimit():Number
    {
    return _cacheLimit;
    }

    public function ImageCacheUtility()
    {
    if (com.thanksmister.caching.ImageCacheUtility.imageCache != null)
    throw new Error(“Only one instance should be instantiated”);
    }

    public static function getInstance():com.thanksmister.caching.ImageCacheUtility
    {
    if (imageCache == null)
    imageCache = new com.thanksmister.caching.ImageCacheUtility();

    return imageCache;
    }

    public function cacheImage(id:String, source:Image):void
    {
    for each (var newObj:Object in imageDictionary)
    {
    if (newObj.id == id)
    return;
    }
    var bd:BitmapData = getBitmapData(source);
    var obj:Object = new Object();
    obj.id = id;
    obj.data = bd;
    imageDictionary.addItem(obj);
    checkLimit();

    if(_currentlyRequestedURLs.contains(id))
    {
    _currentlyRequestedURLs.removeItemAt(_currentlyRequestedURLs.getItemIndex(id));
    var bm:Bitmap = new Bitmap(bd);
    dispatchRequestComplete(id, bm);
    }
    }

    private var _currentlyRequestedURLs:ArrayCollection = new ArrayCollection();

    public function requestImage(url:String):void
    {
    var bm:Bitmap;
    for each (var obj:Object in imageDictionary)
    {
    if (obj.id == url)
    {
    bm = new Bitmap(obj.data);
    }
    }

    if (bm)
    {
    dispatchRequestComplete(url, bm);
    }
    else
    {
    if (!_currentlyRequestedURLs.contains(url))
    {
    _currentlyRequestedURLs.addItem(url);
    dispatchRequestComplete(url, url);
    }
    }
    }

    private function dispatchRequestComplete(url:String, result:Object):void
    {
    dispatchEvent(new ImageCacheEvent(ImageCacheEvent.COMPLETE, url, result));
    }

    private function getBitmapData(target:Image):BitmapData
    {
    var bd:BitmapData = Bitmap(target.content).bitmapData;
    return bd;
    }

    public function clear():void
    {
    imageDictionary.removeAll();
    }

    public function removeImage(id:String):void
    {
    var i:Number = 0;
    for each (var obj:Object in imageDictionary)
    {
    if (obj.id == id)
    {
    imageDictionary.removeItemAt(i);
    return;
    }
    i++
    }
    }

    private function checkLimit():void
    {
    var i:Number = 0;
    while (imageDictionary.length > _cacheLimit)
    {
    imageDictionary.removeItemAt(i);
    i++;
    }
    }
    }
    }
    [/as]

    ImageCache.as:
    [as]
    /*
    ImageCache
    version 1.0.0
    Created by Michael Ritchie (Mister)
    mister@thanksmister.com
    http://www.thanksmister.com

    Simple component to cache Images loaded from URLs. This could
    easily be expanded to cache any type of information loaded into
    the Image control. For more information, check Ely Greenfields post:

    http://www.quietlyscheming.com/blog/2007/01/29/new-flex-componentsample-superimage/

    This is release under a Creative Commons license. More information can be
    found here:

    http://creativecommons.org/licenses/by/2.5/
    */

    package com.thanksmister.controls
    {
    import com.thanksmister.caching.ImageCacheUtility;
    import com.thanksmister.events.ImageCacheEvent;

    import flash.display.Bitmap;
    import flash.display.LoaderInfo;
    import flash.events.Event;
    import flash.events.IOErrorEvent;
    import flash.system.LoaderContext;
    import flash.utils.setTimeout;

    import mx.controls.Image;
    import mx.core.mx_internal;

    use namespace mx_internal;

    public class ImageCache extends Image
    {
    public function ImageCache()
    {
    super();
    super.addEventListener(IOErrorEvent.IO_ERROR, onIOError);
    }
    private var imageCache:ImageCacheUtility = ImageCacheUtility.getInstance();
    private var _sourceURL:String = “”;
    private var _cacheLimit:Number = 100;
    private var _loadingTriesCounter:int;
    private const MAX_LOADING_TRIES:int = 10;

    public function get cacheLimit():Number
    {
    return _cacheLimit;
    }

    public function set cacheLimit(num:Number):void
    {
    _cacheLimit = num;
    imageCache.cacheLimit = num;
    }

    // Clears the cached completely
    public function clearChache():void
    {
    imageCache.clear();
    }

    public override function set source(value:Object):void
    {
    if (value is String && String(value) != “”)
    {
    _sourceURL = String(value);
    imageCache.addEventListener(ImageCacheEvent.COMPLETE, onImageRequestComplete);
    imageCache.requestImage(String(value));

    var lc:LoaderContext = new LoaderContext();
    lc.checkPolicyFile = true;
    super.loaderContext = lc;
    }
    }

    private function onImageRequestComplete(event:ImageCacheEvent):void
    {
    if (_sourceURL == event.url)
    {
    imageCache.removeEventListener(ImageCacheEvent.COMPLETE, onImageRequestComplete);
    if(event.result is Bitmap)
    {
    super.source = new Bitmap(Bitmap(event.result).bitmapData);
    }
    else
    {
    super.source = event.result;
    }

    if (event.result is Bitmap)
    callLater(dispatchEvent, [new Event(Event.COMPLETE)]);
    }
    }

    private function onIOError(event:IOErrorEvent):void
    {
    _loadingTriesCounter++;
    if (_loadingTriesCounter <= MAX_LOADING_TRIES)
    {
    event.stopImmediatePropagation();
    setTimeout(setSource, 1000, _sourceURL);
    }
    }

    private function setSource(url:String):void
    {
    source = null;
    source = _sourceURL;
    }

    // If we have a URL then cache a bitmap of this control with the
    // url string as the identifier
    override mx_internal function contentLoaderInfo_completeEventHandler(event:Event):void
    {
    if (LoaderInfo(event.target).loader != contentHolder)
    return;

    if (_sourceURL != "")
    {
    imageCache.cacheImage(_sourceURL, this);
    }

    //var smoothLoader:Loader = event.target.loader as Loader;
    // var smoothImage:Bitmap = smoothLoader.content as Bitmap;
    // smoothImage.smoothing = true;

    super.contentLoaderInfo_completeEventHandler(event);
    }
    }
    }
    [/as]

    ImageCacheEvent.as (NEW):
    [as]
    package com.thanksmister.events
    {
    import flash.events.Event;

    public class ImageCacheEvent extends Event
    {
    public static const COMPLETE:String = "complete";

    public var result:Object;
    public var url:String;

    public function ImageCacheEvent(type:String, url:String, result:Object, bubbles:Boolean = false, cancelable:Boolean = false)
    {
    super(type, bubbles, cancelable);
    this.result = result;
    this.url = url;
    }
    }
    }
    [/as]

  10. very handy – thanks!

    Made a small update to ImageCache.as (noticed the last retrieved image was displayed if the url was null):

    public override function set source(value:Object):void
    {
    if (value is String && String(value) != “”)
    {
    _sourceURL = String(value);
    imageCache.addEventListener(ImageCacheEvent.COMPLETE, onImageRequestComplete);
    imageCache.requestImage(String(value));

    var lc:LoaderContext = new LoaderContext();
    lc.checkPolicyFile = true;
    super.loaderContext = lc;
    }else{
    super.source = null;
    }
    }

    1. I noticed an error with Embedded resources that was not present in the original version, doing something like this fixes it:

      public override function set source(value:Object):void
      {
      if(values is Class
      {
      super.source = value;
      }
      else if ((value is String) && String(value) != “”)
      {
      _sourceURL = String(value);
      ImageCacheUtility.getInstance().addEventListener(ImageCacheEvent.COMPLETE, onImageRequestComplete);
      ImageCacheUtility.getInstance().requestImage(String(value));

      var lc:LoaderContext = new LoaderContext();
      lc.checkPolicyFile = true;
      super.loaderContext = lc;
      }
      else
      super.source = null;
      }

  11. Althought astrumit’s code update is a good contribution, I have found that it doesn’t work well with a large amount of images and quick scrolling (swapping out the source). It seems that the cache loses track of the loader state and starts to display the same image for each itemrenderer, say in a Tile List.

  12. Thank you for this very good class. There is only one minor issue for us:

    We display the flight information with airline logos in DataGrid. From time to time, one or two airline logos will be blank. If we restart the web browser this blank logo will be good, but maybe another logo will be blank. I guess we need to use com.thanksmister.controls.ImageCacheRenderer, not the ordinary ItemRenderer. Where can we get your ImageCacheRenderer?

    Thank you in advance and have a nice holiday.

    Daniel

    1. The ImageCacheRenderer is in the zip file downloadable from this post. See the link at the end of the post.

  13. Hello there,

    Cool your work and the collaboration of readers.
    Just a little observation about your Singleton, maybe could be more efficient something like this:

    private static var imageCache:com.thanksmister.caching.ImageCacheUtility = new com.thanksmister.caching.ImageCacheUtility();
    .
    .
    .
    public function ImageCacheUtility()
    {
    if (com.thanksmister.caching.ImageCacheUtility.imageCache)
    	throw new Error("cannot call constructor directly");
    }
    
    public static function getInstance():com.thanksmister.caching.ImageCacheUtility
    {
    	return imageCache;
    }
    

  14. If you want to save the cache to disk when using Abode Air, (So that you still have some images when the net connection fails, for exemple) you may add this two methods to the ImageCacheUtility Class.

    You could also call the unserialize method in the getInstance method, and the serialize when appropriate, i.e. when closing the app.

    pastebin: http://pastebin.com/z90LLB72

    the code in case Pastebin deletes the paste.

    Cheers!

    Rui Pires

    		public function serialize():void
    		{	
    			// save the instance
    			
    			var f:File = File.applicationStorageDirectory.resolvePath('imageCache.obj');
    			
    			var s:FileStream = new FileStream();
    			s.open(f, FileMode.WRITE);
    			
    			// now convert the dict to a format we can sereialize
    			var writeDict:ArrayCollection = new ArrayCollection();
    			
    			for each (var obj:Object in imageDictionary)
    			{
    				// convert bitmap data
    				var bytes:ByteArray = new ByteArray();
    				bytes.writeUnsignedInt(obj.data.width); // store width of image
    				bytes.writeBytes(obj.data.getPixels(obj.data.rect)); // store bitmapdata as bytearray
    				bytes.compress();
    								
    				var saveObj:Object = new Object();
    				saveObj.bytes = bytes;
    				saveObj.id = obj.id;
    				
    				writeDict.addItem(saveObj);
    			}
    			
    			s.writeObject(writeDict);
    		}
    		
    		public function unserialize():void
    		{
    			registerClassAlias('BitmapData', BitmapData);
    			
    			var f:File = File.applicationStorageDirectory.resolvePath('imageCache.obj');
    			
    			var s:FileStream = new FileStream();
    			s.open(f, FileMode.READ);
    			
    			var readDict:ArrayCollection = s.readObject() as ArrayCollection;
    			var newDict:ArrayCollection = new ArrayCollection();
    			
    			// convert the byteaerray bitmapdata to a new imageDict
    			
    			for each (var readObj:Object in readDict)
    			{
    				var rawData:ByteArray = readObj.bytes as ByteArray;
    				rawData.uncompress();
    				
    				var width:int = rawData.readUnsignedInt(); // first 4 bytes (unsigned integer)
    				var height:int = ((rawData.length - 4) / 4) / width;
    				
    				var bmd:BitmapData = new BitmapData(width, height, true, 0); // 32 bit transparent bitmap
    				bmd.setPixels(bmd.rect, rawData); // position of data is now at 5th byte
    				
    				var convertedObj:Object = new Object();
    				convertedObj.data = bmd;
    				convertedObj.id = readObj.id;
    				
    				newDict.addItem(convertedObj);
    				
    			}
    			
    			// we can now use this data instead of the old data
    			imageDictionary = newDict;
    		}
    

  15. Changing these lines in ImageCacheUtility.as makes it WAAAAAAAAY QUICKER!

    Line 87:
    private function getBitmapData( target : Image ) : BitmapData
    {
    var bd:BitmapData = Bitmap(target.content).bitmapData;
    return bd;
    }

    Line 35:
    private var _cacheLimit:Number = 10000; //why not

    Thanks for this great image caching class!

Comments are closed.