AMF is great for serializing AS3 objects. Its compact binary format is far more efficient than XML or JSON and it’s just as easy to use: just call writeObject or readObject. However, there are many ways to make it even more efficient. Today’s article presents one more way that eliminates some overhead you might not have thought out. Read on to learn more and for a helper class that will enable you to avoid it.

The hidden overhead this article is about lies in the AMF file’s header. This is where the definitions of all the object types you’re writing to the AMF data are stored. As it turns out, this structural data is quite large and can often dwarf the size of the actual data you’re writing to AMF. Each time you call writeObject (e.g. on a ByteArray), this header is written out. The header is not shared across writeObject calls.

This leads to a new technique that allows you to call writeObject just once. The implementation is simple, too, which means it has low overhead on your code SWF size and low complexity for easy maintenance and upgrading. All that needs to be done is to replace your writeObject calls on the IDataOutput (e.g. ByteArray) with calls to writeObject on a new AMFObjectPooler class. This class simply implements a pool of objects backed by an Array. When you’re done writing objects, just call its finalize and it will write out all the objects at once with only one writeObject call and one AMF header. To read the objects back in, just call the static read method.

Here’s the source code for AMFObjectPooler:

package
{
	import flash.utils.IDataInput;
	import flash.utils.IDataOutput;
 
	/**
	* Pooler of objects to be written to AMF data. Allows for a single
	* writeObject call and therefore reduces the AMF header data size by
	* consolidating it.
	* @author Jackson Dunstan, http://JacksonDunstan.com
	* @license MIT
	*/
	public class AMFObjectPooler
	{
		/** Objects to write */
		private var __objects:Array = [];
 
		/**
		* Add an object to the pool
		* @param obj Object to add
		*/
		public function writeObject(obj:*): void
		{
			__objects.push(obj);
		}
 
		/**
		* Write the pool of objects to an output stream and optionally clear the
		* pool of objects
		* @param output Output stream to write the objects to
		*/
		public function finalize(output:IDataOutput, clear:Boolean=true): void
		{
			output.writeObject(__objects);
			if (clear)
			{
				__objects.length = 0;
			}
		}
 
		/**
		* Read a pool of objects
		* @param input Input stream to read from
		* @return The pool of objects read from the given input stream
		*/
		public static function read(input:IDataInput): Array
		{
			return input.readObject() as Array;
		}
	}
}

I’ve written a little test app to try this out:

package
{
	import flash.display.Sprite;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
	import flash.net.registerClassAlias;
	import flash.utils.ByteArray;
 
	import net.somebody.foo.goo.utils.SomeClassWithAReallyReallyLongName;
 
	public class AMFPool extends Sprite
	{
		private var logger:TextField = new TextField();
		private function row(...cols): void
		{
			logger.appendText(cols.join(",")+"\n");
		}
 
		public function AMFPool()
		{
			logger.autoSize = TextFieldAutoSize.LEFT;
			addChild(logger);
 
			row("Method", "Size");
			registerClassAlias("C", SomeClassWithAReallyReallyLongName);
			const NUM_OBJECTS:int = 1000;
			var bytes:ByteArray;
			var i:int;
 
			bytes = new ByteArray();
			for (i = 0; i < NUM_OBJECTS; ++i)
			{
				bytes.writeObject(new SomeClassWithAReallyReallyLongName());
			}
			row("Individual", bytes.length);
 
			bytes = new ByteArray();
			var pooler:AMFObjectPooler = new AMFObjectPooler();
			for (i = 0; i < NUM_OBJECTS; ++i)
			{
				pooler.writeObject(new SomeClassWithAReallyReallyLongName());
			}
			pooler.finalize(bytes);
			row("Pool", bytes.length);
		}
	}
}
package net.somebody.foo.goo.utils
{
	public class SomeClassWithAReallyReallyLongName
	{
		public var integerValueWithAReallyLongName:int;
		public var stringValueWithAReallyLongName:String;
		public var booleanValueWithAReallyLongName:Boolean;
		public var numberValueWithAReallyLongName:Number;
	}
}

And here are the results:

Method Size
Individual 143000
Pool 15132

AMF Object Pooling Comparison Chart

Clearly, there’s a huge savings to be had with AMFObjectPooler. This example shows a ~9.4x reduction in AMF data size with almost no change the source code>. If you’re using multiple writeObject calls to a single IDataOutput, why not switch to AMFObjectPooler?

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