Using ByteArray to Serialize AS3 Classes for AIR, Web and Everywhere!
When using AIR's FileStream to write AS3 classes to ByteArray, I've found that the same file format works for loading in via URLLoader, as well as what's uploaded using FTP. This isn't surprising, but wanted to test anyway.
What this means is that ByteArray is a groovy ubiquitous format for storing full classes and dependencies and references, via ByteArray.writeObject and ByteArray.readObject (provided registerClass is used), without nececessarily having to parse them manually though that is still suggested for versioning and class evolution. Meaning you can create a file using AIR , send that file to a friend in email, and have them open it up in a flash based webpage, and they operating in a webpage could Http POST the change back to you. The ByteArray can have rich complexity, classes, self references, collections, etc. This could be used to save whole game states (even massive multiplayer, NPC brains, etc). Or in my target case debugging outside other non-AIR runtimes to speed development.
How to save to the same directory the swf AIR is running from
This took me a bit to figure out. Primarily as I'm using AIR to write the files in Flash CS3 and it's not immediately apparent in the docs, how to target the 'self' directory of the swf. This isn't recommended in the docs (which I agree) but in the case I'm using it for it makes sense to do so. The API for AIR is here
-
-
// this is in url path
-
var file:File = File.applicationDirectory.resolvePath( "example.jpg" );
-
trace("file " + file.url + " " +file.nativePath ); //outputs app:/example.jpg, and "C:\SomeDirectory\Somepath\example.jpg"
-
-
var wr:File = new File(file.nativePath);
-
// Create a stream and open the file for asynchronous reading
-
var stream:FileStream = new FileStream();
-
stream.open( wr, FileMode.WRITE );
-
stream.write.....
-
stream.close();
-
This would be swf._url in AS2, or in AS3 DisplayObject.loaderInfo.url , as when using relative links in URLLoader to load whatever you're saving this will resolve properly in AIR. Turns out flash.filesystem.File.applicationDirectory is the droid you are looking for. It's not fully qualified (e.g. file:C:\somedirectory...) when traced it looks like:
"app-resource:/yourSwfNameGoesHere.swf"
unless you use the file.nativePath which is required in order to actually write to the directory. So you can use still this in old school string name parsing the swf name out, so you don't have to use File.applicationResourceDirectory if your looking to make a single swf for both AIR and Web use, and on the web, the AIR libraries won't be accessible.
Here's two handy scripts to help inspect ByteArray's, the first looks at the 1's an 0's, the other looks at the objects in the stream, using the getQualifiedClassName, and if it finds an Array or sub ByteArray attempts to inspect it.
/* converts byteArray to binary like
TraceBits--------------------
0 = 110
1 = 1011
2 = 1001000
3 = 1100101
*/
-
-
function traceBits(bA:ByteArray) {
-
trace("TraceBits--------------------");
-
for (var i:int = 0; i < bA.length; i++) {
-
bA.position = i;
-
-
var inte:uint = bA.readByte();
-
trace(i+ " = " + inte.toString(2));
-
}
-
}
-
-
function traceByteArray(bA:ByteArray) {
-
var o;
-
var cnt:int = 0;
-
var cnm:String;
-
-
var o2;
-
var cnt2:int = 0;
-
var cnm2:String;
-
try {
-
while (true) {
-
o = bA.readObject();
-
cnm = getQualifiedClassName(o);
-
trace( cnt++ + " " + o + " : " + cnm);
-
if (cnm == "flash.utils::ByteArray") {
-
try {
-
cnt2 = 0;
-
cnm2 = "";
-
while (true) {
-
o2 = o.readObject();
-
trace(" " + cnt2++ + " " + o2 + " : " + getQualifiedClassName(o2));
-
-
}
-
} catch (err:Error) {
-
}
-
} else if (cnm == "Array") {
-
trace("array contents...");
-
try {
-
var ar:Array = o as Array;
-
trace("ar " + ar);
-
trace(ar.join("\r"));
-
} catch (err:Error) {
-
trace(err.toString());
-
}
-
}
-
}
-
} catch (err:Error) {
-
}
-
}
Flash on the Desktop: Zinc vrs Air vrs Director vrs F-in-Box
MDM Multimedia put there, with their comparison of AIR and Zinc, and I It's accurate but I feel an incomplete pitch. AIR doesn't really expect much more users than they currently expect with the flash plugin, and they expect much less from developers working in AIR, than Zinc or other lanaguages.
OTHER POINTS FOR AIR
- Integrated with Flex and Flash production tools. 1 step publish. Zinc, F-in-box, Director have many more steps and over the course of a project it adds up.
- integrated PDF and HTML and Javascript, with cross language communication.
- has a SQL database built into the runtime, no need to setup one.
- very quick installers and install from web.
- it can load/play anything flash can play, and will support the same codec as quictime, which shortly will probably represent 80% of the video on the web, youtube is already streaming their content for iPhone consumption.
The first two I think are very important. It opens up desktop development to web developers which has normally been an different tier of, and it does it much faster than Java or C+ workflows. Within minutes and only a paragraph of code one can create simple tools for text and image manipulation. Flash is great, but it's not necessarily ideal for text or layout heavy docs (e.g. tables). It can also be hard to source talent for. HTML on the other hand most kids in junior high can code.
F-in-Box's key points are, ability to stream movies and FLV from memory...nothing ever hits the disk. Write in Delphi, C, etc. But this requires someone skilled in one of those 'hard' languages, familiar with memory management.
Director is the lowest of my list at this stage as it has not kept up, but it has 3d support and has had stable cross platform desktop access for years. I think it's weak point are windowing, it's primarily kiosk mode or nothing.
Local Shared Objects…not so Shared. Cross Domain Projector Issues.
We have a a few Desktop Flash applications: One is a main application, the other an update utility for the main app. They use a Flash Cookie to find each other, as the main app could be installed anywhere (USB, C), so it writes to the cookie where it's installed at. The apps are based in Director using the Flash Xtra.
Say we start with a completely empty SharedObjects folder
Running the main app sets a cookie into a folder like
....Flash Player\#SharedObjects\BFYLPV7P\localhost\02000.sol
via a call like
var so : SharedObject = SharedObject.getLocal ("02000", "/");
where "/" should see the root of the Flash Cookies.
then say Pandora (or any other flash based website using cookies) runs it generates a new sandbox
....Flash Player\#SharedObjects\2S7GJDMQ\pandora.com
and then all future read requests are against that new '2S7GJDMQ' folder instead of the "BFYLPV7P" folder, and then neither the app or the updater, can see the old shared local object folder anymore. Which in the case of it containing lots of persistent configuration data, is the equivalent of clearing the Browser cache and forgetting everything.
Trying to understand how it's created, I can delete the contents of the shared Objects folder and then republish in the Flash IDE and get a new hash for the folder every time so it seems to be either random or time based. It's not as far as I can tell the behaviour documented, it's certainly not behaving as I would expect.
As mentioned here. It used to be that one could work around this via:
SharedObject.getRemote("my_so", "rtmp:/./..", "/");
sadly it no longer works in Flash 8, or 9.
I got this reply on the FlashCoders list:
>Movies running in different sandboxes cannot access shared objects of
>each other.
Yes, that makes sense, except for 2 things:
1) As far as I can tell, they should NOT be in separate sandboxes, they
use the exact same code, and are installed in next to the same folder. As far
as I can tell there is no way to specify the domain for a projector to get the
desired behavior.
2) It's also inconsistent. First run it WILL use the same sandbox as the Flash IDE/etc, then later something will trigger the generation of a new sandbox (install a second version of the app, or view pandora) then all further requests go against the new sandbox instead of the original. Meaning the Flash Player changes it's mind about which domain it thinks it belong to!
My best guess, likely due to it running in a projector without access to URL information, it's making stuff up.
Our first attempt at solving, required upgrading the Flash Xtra in both projectors to Flash8, apparently having one at Flash7 and the other Flash8 is enough to trigger the domain.
Also interesting is the Flash IDE never really seems to have this problem, presumably because it is using the Flash Player EXE as the handler for all swfs via the registry. So one potential workaround was firing a Flash Projector with the sole point in life to read/write the local Shared Object, with local connection object to read how flash sees it and then shut down.
Flash on the Desktop…everywhere. Flash CS3 supports AIR output
This week has been week of Desktop Based Flash. 1) Using Director + the Buddy API Xtra, 2) a client with a C++ and F-in-Box, and just finishing up save capability 3) AIR Local File Access for Flash
Testing applications inside of these projectors can really slow down application development as it's compile in flash, and then test in the projector.
Still I'm quite stoked to see that Flash IDE has an update to support building AIR apps in the IDE. This rocks for several reasons as many apps I make are based off XML configuration or could benefit from making tools to help generate the data. Here is a good post on actually using the update to create a hello world example.
This is after spending a week battling with bizarre Shared Object behavior. Memo to self, don't depend on SharedObjects for reliable desktop storage!
Serialization: IExernalizable and Evolving Classes
What is evolving a class? It's the inevitable:
- addition of a field
- change the superclass
- remove a field
- change the name of a field
- change a field to static (from non-static)
- change a field to transient (from non-transient)
- change the type of a field type (e.g. int to float)
As soon as you do any of the above whatever you've previously saved something to disk, be it local or remote will break upon loading back in.
This isn't common in most web based flash development which lives in isolation on a webpage and doesn't save anything, but introduce any sort of saving. With Desktop Flash apps (AIR, Zinc), and RIA's, offline online applications, it IS becoming something that is important.
There are 3 different strategies to serializing objects, only one is particularly useful for evolving classes.
- Direct:
writeObject(yourClassObject); - Customize with IExternalizeable: iterate over each attribute of your object and write each of those to the stream.
- Customize with IExternalizeable, iterate over each attribute of your object write it with a key to a Dictionary / HashTable.
Approach 1 is great for simple uses and first passes. However it doesn't scale well or at all. Approach 2 Is better but as the stream is sequential, this can make dealing with multiple versions difficult or impossible as you have to know the exact order in which to parse. Approach 3 is the best, it uses a Dictionary as an intermediate map, it's only a bit more code to write, and is a *whole* lot easier to parse. This is also the approach that Remoting uses when talking to the server with the ASObject, and in someways writing to disk is identical to writing to the filesystem.
Here is the list of imports:
-
-
import flash.utils.Dictionary;
-
import reflection.introspectable;
-
import flash.utils.IExternalizable;
-
import flash.utils.IDataInput;
-
import flash.utils.IDataOutput;
-
import flash.net.registerClassAlias;
Here is the version number, we'd need to increase everytime a significant data change has happened, and some of the static register class necessary to get the class to serialize and deserialize correctly:
-
-
public static const serialVersionUID:Number = 01;
-
private static const REG_DICT:* = registerClassAlias("flash.utils.Dictionary",Dictionary);
-
private static const REG:* = registerClassAlias("reflection.IntrospectableObj",IntrospectableObj);
-
[RemoteClass(alias="reflection.IntrospectableObj")]
-
Here are the modified IExternalizable functions. Note that the read from the stream has to deal with all permutations for a given version to version, as this number of changes gets large, the normalization can be quite large, thankfully good architecture up front can prevent much of this.
-
-
///////////////////////////////////////////////
-
public function readExternal(input:IDataInput):void
-
{
-
var d:Dictionary = input.readObject();
-
var serialVersionUID:Number = d["serialVersionUID"] as Number ;
-
trace("readExternal " + serialVersionUID);
-
/////////////// COMMON TO ALL VERSIONS ///////////////////////////
-
if(serialVersionUID == 1 || serialVersionUID == 2 || serialVersionUID == 3){
-
introspectable::introVarStr = d["introVarStr"]as String;
-
defVarStr = d["defVarStr"]as String;
-
-
pubByteArray =d["pubByteArray"] as ByteArray;
-
pubBitmapData= (d["pubBitmapData"] as BitmapDataWrapper).getBitMapData();
-
}
-
/////////////// COMMON TO TWO VERSIONS //////////////////////
-
if(serialVersionUID == 1 || serialVersionUID == 2){
-
}
-
-
if(serialVersionUID == 2 || serialVersionUID == 3){
-
-
}
-
if(serialVersionUID == 1 || serialVersionUID == 3){
-
-
}
-
///////////// COMMON TO ONE VERSION /////////////////////////
-
if(serialVersionUID == 1 ){
-
-
}else if(serialVersionUID == 2 ){
-
}else if(serialVersionUID == 3 ){
-
}
-
-
}
-
-
public function writeExternal(output:IDataOutput):void
-
{
-
trace("writeExternal....");
-
var d:Dictionary = new Dictionary();
-
d["serialVersionUID"] = serialVersionUID;
-
-
// name spaced var
-
d["introVarStr"] = introspectable::introVarStr;
-
d["defVarStr"] = defVarStr;
-
-
// this has already packed data
-
d["pubByteArray"] = pubByteArray;
-
-
// since BitmapData can't be retrieved, we've created a wrapper that can be.
-
d["pubBitmapData"] = new BitmapDataWrapper(pubBitmapData);
-
output.writeObject(d);
-
}
AS3 Customized Serialization and Deserialization, IExternalizeable to the rescue!
This and original off Pete's blog is great article on a little known feature critical for de/serialization in flash, and the knowledge is not just for client/server as the The flex documentation on the subject implies. Anywhere the AMF format is used: shared local objects, local connection, in AIR using fileStream. cloning objects using ByteArray etc. So many gems (might as well be easter eggs!) in the AS3 code, sadly barely documented. It's using IExternalizeable for custom serialization, which mimicks Java's already proven approach.
What's that mean? It means you can better control how objects are written and read back from the byteStream.
What if I don't want X to be written?
To compliment a great follow up on using the [Transient] metadata by Darron, with both the clone and when serializing to disk or the web. I've got to say the metadata format makes sense but also wierds me out.
How do I get my class type back
registerClassAlias is necessary for local cloning and maintaining Class type, when written to a ByteArray they forget which class they belong to. I would have hoped that the RemoteClass metadata would have worked but that was not the case. Anyway you can do this via a static var:
private static const REG:* = registerClassAlias("reflection.IntrospectableObj",IntrospectableObj);
this is called during static initialization, once which is all that's really needed as the linkage is static, not per instance.
What if you need to transcend versions of code?
basic approach to using serialization to deal with different versions of the class is
1) write a version number to the stream first, then a) save to hashmap, or b) iterate over the members that should be saved.
2) read the version number back from the stream, then reconstitue the members from a) the Dictionary (like Remotings's ASObject) or b) in the order they were written to the stream. Vary the parsing response upon the version number.
Summary of Serialization Tips:
1) Use IExternalizable to customize how your objects are written.
2) Use the [Transient] metadata to NOT serialize objects if you don't want them there, and are using writeObject().
3) registerClassAlias is necessary for local cloning and maintaining Class type. You can do this via a static var,
private static const REG:* = registerClassAlias("reflection.IntrospectableObj",IntrospectableObj);
4) keep a unique version number in the code, write a version first in the output stream then when reading it back perform different behavior based on that stream. The use of a Dictionary for key/values can be helpful too.
