AMF Serialization Tricks
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 |
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!
#1 by Mike Keesey on May 20th, 2013 ·
Of course this also means that your server component must replicate the same serialization logic (unless you just plan to used it for SharedObject storage or something).
#2 by jackson on May 20th, 2013 ·
That’s a really good point. Since you’re not sending standard AMF for any class you implement
IExternalizable
for, you’ll need to do the same custom deserialization in any code (e.g. server) that handles the serialized data, if any.#3 by caius on May 26th, 2013 ·
I have been playing with this code for some hours and these are my conclusions:
Element size 25
ElementIEByte size 17
1. changed public var atomicNumber:uint; -> change to public var t:uint; in both classes and got the following result:
Element size 14
ElementIEByte size 17
– so for very long varbile names, it is better to write your own class. For short variable names the default AMF serialization is better.
2. changed
to
and the output was:
Element size 14
ElementIEByte size 5
– so changing the aliasName hugely reduces the size of the ByteArray
#4 by jackson on May 26th, 2013 ·
You’ve hit on an important fact that I didn’t explicitly mention in the article, namely that variable names and class names are stored in the serialized AMF data. To avoid this for the class names, I called
registerClassAlias
with identical length names. Storing these strings can be pretty large, which is another reason to customize the AMF output withregisterClassAlias
andIExternalizable
.#5 by caius on May 26th, 2013 ·
Yes, so to further reduce the size of the serialize AMF data is did the following:
registerClassAlias(“1”, com.game.actions.PlayerInputAction);
registerClassAlias(“2”, com.game.actions.PingAction);
registerClassAlias(“3”, com.game.actions.GameStateAction);
dirty but efficient, because this reduced the size of the bytearray from 107 to 20 !!! :)
The classAlias is used by the serialization/deserialization to identify an object based on that name. This is just a name for both client and server to identify an object, afterwards it will make the cast to the class specified by “classObject” from registerClassAlias.
Also tried to increase the length of the variable names and the class name but it did not affect the size of the bytearray, because you are sending bytes with an alias. When the bytes are received, it searches for that alias and deserializes that object to the specific type.
#6 by Newbie on November 25th, 2015 ·
This page helped me tremendously! Thank you!