Reduce AMF Bloat With AMFObjectPooler
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 |
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!
#1 by Adam on November 1st, 2013 ·
Would you consider posting example code which shows how this process would work with several differently typed objects? I am curious to see how you connect each object back to its original type and how you associate the typed object back to its original client.
#2 by jackson on November 1st, 2013 ·
Here’s an example:
Basically, if you use
registerClassAlias
then the type of the object will be preserved. This means when you read it back in you can use theis
operator to inspect its type.For some more detail, check out Serialize Anything an How To Minimize AMF Serialization Size.