AMF is great in its stock configuration, but there are some little-known tricks to make it even better. Today’s article shows you how to customize the serialization and deserialization of objects to achieve even smaller file sizes and gain maximum control.

While the AMF format tends to produce smaller files than the JSON and XML formats, such competition isn’t really known for minimizing file size. AMF works well for general-case objects, but there are many cases where we have specialized information about particular objects. For example, consider an Element class for an atomic element (i.e. from the periodic table):

class Element
{
	public var symbol:String;
	public var atomicNumber:uint;
}

It’s unfortunate that AS3 doesn’t allow us to use smaller integer types as 32 bits is overkill for the 0-119 range necessary for atomic numbers. Even considering that new elements will be discovered, a single byte can handle the 0-255 range and provide plenty of room to grow.

As it turns out, though AS3 doesn’t provide us with a smaller integer type, we can store smaller integers to ByteArray or any other IDataOutput. Where this ties into AMF is that we can implement the flash.utils.IExternalizable interface with any class and customize the serialization of any class. That is, we don’t need to rely on the default serialization that AMF would perform but instead we can provide a public function writeExternal(out:IDataOutput): void to write arbitrary data and a public function readExternal(in:IDataInput): void to read that arbitrary data back in during deserialization.

We therefore have a very convenient way to customize the serialization format of arbitrary classes and still use AMF via such methods as ByteArray.writeObject/readObject. Further, we don’t have to customize all of our classes, only the ones we choose to. The bulk of your classes are probably fine with the default AMF format, but when you want to optimize you can simply customize one or two classes as needed.

In the case of the Element class, here’s how we would customize so that only a single byte is serialized for the atomic number:

class ElementIEByte implements IExternalizable
{
	public var symbol:String;
	public var atomicNumber:uint;
 
	public function writeExternal(output:IDataOutput): void
	{
		output.writeUTF(symbol);
		output.writeByte(atomicNumber);
	}
 
	public function readExternal(input:IDataInput): void
	{
		symbol = input.readUTF();
		atomicNumber = input.readUnsignedByte();
	}
}

In theory, this will save three bytes for each Element that is serialized. Let’s put it to the test and throw in another version that uses IExternalizable but writes out a full 32-bit integer:

package
{
	import flash.net.registerClassAlias;
	import flash.utils.ByteArray;
	import flash.display.*;
	import flash.text.*;
 
	public class TestIExternalizable extends Sprite
	{
		private var __logger:TextField = new TextField();
		private function row(...cols): void
		{
			__logger.appendText(cols.join(",")+"\n");
		}
 
		public function TestIExternalizable()
		{
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
 
			__logger.autoSize = TextFieldAutoSize.LEFT;
			addChild(__logger);
 
			row("Serializing to check size...");
			row();
			row("Class", "Size");
 
			registerClassAlias("ElementNormal", Element);
			registerClassAlias("ElementIExByt", ElementIEByte);
			registerClassAlias("ElementIExInt", ElementIEInt);
 
			var elem:Element = new Element();
			elem.symbol = "H";
			elem.atomicNumber = 1;
			var bytesElement:ByteArray = new ByteArray();
			bytesElement.writeObject(elem);
			row("Element size", bytesElement.length);
 
			var elemIEInt:ElementIEInt = new ElementIEInt();
			elemIEInt.symbol = "H";
			elemIEInt.atomicNumber = 1;
			var bytesElemInt:ByteArray = new ByteArray();
			bytesElemInt.writeObject(elemIEInt);
			row("ElementIEInt size", bytesElemInt.length);
 
			var elemIEByte:ElementIEByte = new ElementIEByte();
			elemIEByte.symbol = "H";
			elemIEByte.atomicNumber = 1;
			var bytesElemByte:ByteArray = new ByteArray();
			bytesElemByte.writeObject(elemIEByte);
			row("ElementIEByte size", bytesElemByte.length);
 
			row();
			row("Deserializing to check integrity...");
			row();
 
			bytesElement.position = 0;
			var elemCheck:Element = bytesElement.readObject() as Element;
			row(
				"Element",
				(elem.symbol == elemCheck.symbol
				&& elem.atomicNumber == elemCheck.atomicNumber)
					? "pass"
					: "fail"
			);
 
			bytesElemInt.position = 0;
			var elemIntCheck:ElementIEInt = bytesElemInt.readObject() as ElementIEInt;
			row(
				"ElementIEInt",
				(elemIEInt.symbol == elemIntCheck.symbol
				&& elemIEInt.atomicNumber == elemIntCheck.atomicNumber)
					? "pass"
					: "fail"
			);
 
			bytesElemByte.position = 0;
			var elemByteCheck:ElementIEByte = bytesElemByte.readObject() as ElementIEByte;
			row(
				"ElementIEByte",
				(elemIEByte.symbol == elemByteCheck.symbol
				&& elemIEByte.atomicNumber == elemByteCheck.atomicNumber)
					? "pass"
					: "fail"
			);
		}
	}
}
import flash.utils.IDataInput;
import flash.utils.IDataOutput;
import flash.utils.IExternalizable;
 
class Element
{
	public var symbol:String;
	public var atomicNumber:uint;
}
 
class ElementIEInt implements IExternalizable
{
	public var symbol:String;
	public var atomicNumber:uint;
 
	public function writeExternal(output:IDataOutput): void
	{
		output.writeUTF(symbol);
		output.writeUnsignedInt(atomicNumber);
	}
 
	public function readExternal(input:IDataInput): void
	{
		symbol = input.readUTF();
		atomicNumber = input.readUnsignedInt();
	}
}
 
class ElementIEByte implements IExternalizable
{
	public var symbol:String;
	public var atomicNumber:uint;
 
	public function writeExternal(output:IDataOutput): void
	{
		output.writeUTF(symbol);
		output.writeByte(atomicNumber);
	}
 
	public function readExternal(input:IDataInput): void
	{
		symbol = input.readUTF();
		atomicNumber = input.readUnsignedByte();
	}
}

All of the deserialization checks at the end pass. Here are the results of the size comparison:

Class Size
Element size 41
ElementIEInt size 23
ElementIEByte size 20

AMF Serialization Sizes

So it turns out that just avoiding the default AMF serialization and writing to the IDataOutput ourselves produces a smaller file size by quite a bit. On top of that, the final version uses a byte instead of an int and we see the extra three bytes of savings that were predicted by the theory.

In conclusion, if you’re using AMF to serialize your class objects and want to cut down on the file size for any reason, consider using IExternalizable to customize the serialization and deserialization process.

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