Típusos adatok kimentésére régen is volt lehetőség erre szolgált a SharedObject ami szintén AMF formátumban mentette az adatainkat azonban a lementett bináris fájlokhoz nem férünk hozzá. Hogyha egyszerűen átláthatóan akartunk adatokat lementeni XML-t vagy más szöveges formátumot használhattunk. AS3-ban továbbra is lehetőség van SharedObject-el menteni és visszatölteni adatokat azonban ha
nekünk egy adatbázisba kellene letárolni a flash mozink egy bizonyos állapotát, sablonokat vagy komplex objektumokat kellene lementeni, használhatjuk a ByteArray osztályt. Ez az osztály hasonlít egy tömb-re csak annyiban különbözik hogy byte-okat tárol és rendelkezik olyan metódusokkal amiknek segítségével különböző adattípusokat lehet beszúrni a byte folyamba. Egyik hasznos metódusa a writeObject(object:*):void aminek segítségével egy AS objektum szúródik be valamint a readObject():* ami pedig kiolvas egy objektumot a position által mutatott pozíciótól kezdve.

A mentés egy módszerét a lenti alkalmazás szemlélteti. Használatához csak hozzá kell adnunk pár adatot a listákhoz, majd vágólapra másolnunk a lent megjelenő szöveget ami lap újratöltés után visszamásolhatunk és a betöltés gombra kattintva viszont láthatjuk a korábban hozzáadott adatainkat.

Típusos adatok mentése

import flash.utils.ByteArray;

var object:Object = new Object()
object.name = "test"
object.num = 1

var bytes:ByteArray = new ByteArray()
bytes.writeObject(object)
trace(bytes.position) 			//21

bytes.position = 0
var newObj:Object = bytes.readObject()
trace(newObj.name) 				// test
trace(newObj.num)  				// 1
trace(newObj.num+newObj.num)	// 2

Ebben a mintában létrehozunk egy sima objektumot amin létrehozunk 2 tulajdonságot majd beleírjuk azt a ByteArray-ba. Amikor ez megtörténik flash átnézi az objektum struktúráját és binárisan leírja a szerkezetét valamint menti a benne található adatok értékeit. Mielőtt visszaolvasnánk a pozíciót be kell állítanunk 0-ra mivel írás után ez a legutolsó pozíciót veszi fel ahol nem kezdődik objektum leírás. Olvasásnál a struktúra elemzésével újra felépíti az objektumot ami ebben az esetben egy pontos másolat lesz. Gyakran használják ezt a módszert másolásra is mivel minden szinten minden tulajdonság újra létrejön és nem fog referenciákat tartalmazni.

import flash.utils.ByteArray;
import flash.utils.getQualifiedClassName;
import flash.display.MovieClip;

var object:Object = new Object()
object.name = "test2"
object.date = new Date()

var items:Array = new Array()
items.push("Elem1")
items.push("Elem2")
object.items = items

var bytes:ByteArray = new ByteArray()
bytes.writeObject(object)
trace(bytes.position) 			//54

bytes.position = 0
var newObj:Object = bytes.readObject()
trace(newObj.name) 				// test2
trace(newObj.items)  			// Elem1,Elem2
trace(getQualifiedClassName(newObj.date)) // Date
trace(newObj.date.fullYear)			//2010

trace(newObj.items == items)	// false

Ebben a mintában már egy tömböt és egy Date-et is tároltam majd visszaolvasás után probléma nélkül használhatóak lettek és a típusukat is megtartották. Az utolsó trace üzenetből látszik hogy az új tömböm nem azonos a mentettel tehát egy pontos másolat keletkezet, ez a másolás több szinten is végrehajtódik. A dátum objektumom is visszakaptam ami szintén megőrizte a típusát és az összes értéket amit tárolt.

A következő lépés hogy hogyan tudnánk a ByteArray tartamát valhogyan String-ként megkapni. Erre létezik a neten sok megvalósítás én ezután a Base64 generic buspar buy formátumot fogom használni. Ha valakinek nem lenne Base64 konvertáló osztálya megtalálhatja az optimalizált verziót itt:
http://code.google.com/p/jpauclair-blog/source/browse/trunk/Experiment/Base64/src/Base64.as vagy letöltheti innen.
Base64.zip
Mivel ez a verzió Vector-t használ aki nem tud flash player 10-re csak 9-re exportálni használjon valami régebbi megvalósítást például ezt:
http://garry-lachman.com/2010/04/21/base64-encoding-class-in-actionscript-3/

Ha megvan a szimpatikus Base64 konvertáló osztály rakjuk a src mappánkba vagy az fla fájlunk mellé és már kezdhetjük is használni. A Base64 bináris adatokhoz karaktereket (64 db) rendel amik mind biztonságosan használhatók webes környezetben. Módosítsuk a kódunkat, hogy most már ezt is használja.

import flash.utils.ByteArray;

var object:Object = new Object()
object.name = "test3"
object.num = 1

var str:String = convertObj(object)
trace(str)						//CgsBB251bQQBCW5hbWUGCXRlc3QB
var newObj:Object = createObj(str)

trace(newObj.name) 				// test3
trace(newObj.num)  				// 1

function convertObj(obj:*):String
{
	try{
		var bytes:ByteArray = new ByteArray()
		bytes.writeObject(obj)
		return Base64.encode(bytes)
	}catch(error:Error){
		trace("Hiba konvertáláskor ", error.message)
	}

	return null
}

function createObj(base64Str:String):*
{
	try{
		var bytes:ByteArray = Base64.decode(base64Str)
		bytes.position = 0
		return bytes.readObject()
	}catch(error:Error){
		trace("Hiba létrehozáskor ", error.message)
	}
	return null
}

A convertObj függvényünk létrehozza a String reprezentációt amit a createObj függvényünk olvas vissza. Látható hogy a 2 művelet közé könnyen beiktatható egy php-val fájlba vagy adatbázisba mentés így lehetőségünk van komplex adatstruktúrákat pár sorral lementeni majd pont olyan egyszerűen visszatölteni a típusok elvesztése nélkül.

Saját osztályok mentése

Amennyiben saját osztályokat kívánunk menteni be kell regisztrálnunk azokat a registerClassAlias(aliasName:String, classObject:Class):void metódus segítségével. Az aliasName egy tetszőleges név lesz amit a player eltárol a kód futása során a classObject pedig a hozzá rendelt osztály. A név minden objektum elkódolásnál elmentődik és visszatöltéskor a player kikeresi a hozzá tartozó osztályt, készít belőle egy példányt majd beállítja rajta a publikus tulajdonságokat.
Használni fogunk 2 minta osztályt amiket elmentünk majd később visszaöltünk továbbá átrakjuk a mentést és a visszatöltés egy külön statikus osztályba.

data/ItemHolder.as
Egyszerű, elemeket tömbben táróló osztály.

package data
{
	public class ItemHolder
	{
		public var items:Array

		public function ItemHolder()
		{
			items = new Array()
		}

		public function addItem(item:*):void
		{
			items.push(item)
		}

		public function getItem(index:int):*
		{
			return items[index]
		}
	}
}

data/DataHolder.as
Egyszerű, kulcs érték párokat objektumban tároló osztály.

package data
{
	public class DataHolder
	{
		public var itemHolder:ItemHolder
		public var properties:Object

		private var _name:String

		public function get name():String { return _name; }
		public function set name(value:String):void{
			_name = value;
		}

		public function DataHolder(name:String=null)
		{
			_name = name

			itemHolder = new ItemHolder()
			properties = new Object()
		}

		public function addProperty(key:String, value:*):void
		{
			properties[key] = value
		}

		public function getProperties(key:String):*
		{
			return properties[key]
		}
	}
}

util/SaveUtil.as

package util
{
	import flash.net.registerClassAlias;
	import flash.utils.ByteArray;

	public class SaveUtil
	{
		public static function register(alias:String, clazz:Class):void
		{
			registerClassAlias(alias, clazz);
		}

		public static function convertObj(obj:*):String
		{
			var bytes:ByteArray = new ByteArray()
			bytes.writeObject(obj)
			return Base64.encode(bytes)
		}

		public static function createObj(base64Str:String):*
		{
			var bytes:ByteArray = Base64.decode(base64Str)
			bytes.position = 0
			return bytes.readObject()
		}
	}
}

Ezek lesznek a majd később mentést szemléltető osztályok, lehet látni hogy a DataHolder tárol egy ItemHolder példányt ami a saját osztályok egymáson belül használatát szemlélteti.
Fontos odafigyelni az osztályok felépítésnél hogy nem rendelkezhet kötelezően megadandó konstruktor paraméterrel mivel akkor nem tud a player új példányt létrehozni belőlük és hibát fogunk kapni. Továbbá a privát tulajdonságok értékei nem mentődnek és állítódnak vissza, tehát ha egy alap értékkel rendelkező konstruktornak egy eltérő értéket adunk át de az nem érhető el se publikus tulajdonságból sem geter, seter segítségével nem fog visszatöltődni. A DataHolder-ben láthatunk erre egy mintát. Mivel a name megadható konstruktorban is de létezik hozzá geter, seter pár ezért visszatöltéskor ez is be fog állítódni de nem a konstruktor hanem a set name segítségével.

A menteni kívánt osztályok regisztrációját még mentés vagy betöltés előtt meg kell tennünk ezek utána minden olyan műveletnél felhasználásra kerülnek ami valamilyen módon bináris formába konvertálja az objektumokat. Az alábbi osztályokban történik ilyen:
LocalConnection, ByteArray, SharedObject, NetConnection, NetStream

import data.DataHolder;
import data.ItemHolder;

import util.SaveUtil;

private function test():void
{
	SaveUtil.register("reg.DataHolder", DataHolder);
	SaveUtil.register("reg.ItemHolder", ItemHolder);

	var dataHolder:DataHolder = new DataHolder("holder")
	dataHolder.addProperty("date", new Date())

	var itemHolder:ItemHolder = new ItemHolder()
	itemHolder.addItem("Elem1")
	itemHolder.addItem("Elem2")

	dataHolder.itemHolder = itemHolder

	var str:String = SaveUtil.convertObj(dataHolder)
	trace("Base64:",str)

	var savedHolder:DataHolder = SaveUtil.createObj(str)
	trace("Name:",savedHolder.name)
	trace("Year:",savedHolder.getProperties("date").fullYear)
	trace("Item0:",savedHolder.itemHolder.getItem(0))
}

A fenti kód futtatva a következő outputot írja a konzolra:

Base64: CjMdcmVnLkRhdGFIb2xkZXIVaXRlbUhvbGRlchVwcm9wZXJ0aWVzCW5hbWUKEx1yZWcuSXRlbUhvbGRlcgtpdGV tcwkFAQYLRWxlbTEGC0VsZW0yCgsBCWRhdGUIAUJyiKcZFLAAAQYNaG9sZGVy
Name: holder
Year: 2010
Item0: Elem1

Komplex adat vagy display objektumok mentése.

Mivel a writeObject alap esetben a paraméterül kapott objektum minden publikus tulajdonságát lementi akkor is ha azokon az alap értékek vannak beállítva, sok felesleges adat mentődne le mondjuk egy Sprite alapú osztály mentésekor amikre semmi szükség. Ez elkerülendő több lehetőségünk is van a feladat megoldására a problémától függően.

IExternalizable interface

Amennyiben saját osztályok kimentését akarjuk megvalósítani amik private tulajdonságokkal is rendelkeznek, lehetőségünk van implemnetálni az IExternalizable interface-t ami 2 publikus metódust definiál.

readExternal(input:IDataInput):void
writeExternal(output:IDataOutput):void

Ezeket metódusokat flash automatikusan hívja meg amikor a kiírás vagy visszaolvasás megtörténik paraméterül pedig IDataOutput,IDataInput típusú objektumot kapnak amikre nekünk kell mentenünk vagy kiolvasnunk róluk a szükséges változókat. Az IDataOutput, IDataInput szintén egy interface ami pár fontosabb kiíró és beolvasó utasítást definiál ezeket közül egyet vagy mindkettőt implementálják a bináris adatmanipulációt megengedő osztályok az adatfolyam irányától függően. Például a ByteArray osztály mindkettőt az URLStream viszont csak az input-ot. Lássuk hogy néznének ki a fent használt osztályok ha private tulajdonságokat is tartalmaznának és implementálnák az IExternalizable interface-t.

data/DataHolder.as

package data
{
	import flash.utils.IDataInput;
	import flash.utils.IDataOutput;
	import flash.utils.IExternalizable;

	public class DataHolderP implements IExternalizable
	{
		private var properties:Object

		private var _itemHolder:ItemHolderP
		private var _name:String

		public function get name():String { return _name; }

		public function get itemHolder():ItemHolderP
		{
			return _itemHolder
		}

		public function DataHolderP(name:String=null)
		{
			_name = name
			_itemHolder = new ItemHolderP()

			properties = new Object()
		}

		public function addProperty(key:String, value:*):void
		{
			properties[key] = value
		}

		public function getProperties(key:String):*
		{
			return properties[key]
		}

		public function writeExternal(output:IDataOutput):void
		{
			output.writeObject(properties)
			output.writeObject(_itemHolder)
			output.writeUTF(_name)
		}

		public function readExternal(input:IDataInput):void
		{
			properties = input.readObject()
			_itemHolder = input.readObject()
			_name = input.readUTF()
		}
	}
}

data/ItemHolder.as

package data
{
	import flash.utils.IDataInput;
	import flash.utils.IDataOutput;
	import flash.utils.IExternalizable;

	public class ItemHolderP implements IExternalizable
	{
		private var items:Array

		public function ItemHolderP()
		{
			items = new Array()
		}

		public function addItem(item:*):void
		{
			items.push(item)
		}

		public function getItem(index:int):*
		{
			return items[index]
		}

		public function writeExternal(output:IDataOutput):void
		{
			output.writeObject(items)
		}

		public function readExternal(input:IDataInput):void
		{
			items = input.readObject()
		}
	}
}

Látható hogy mostmár nekünk kell megírni az adatok mentését majd visszatöltését. Fontos hogy a típusnak megfelelő metódust hívjuk meg és hogy mostmár a publikus tulajdonságok mentéséről is nekünk kell gondoskodnunk. Amennyiben egy olyan osztályból származtatunk amelyik már implementálja az IExternalizable interface-t felül kell írnunk a kiíró és beolvasó metódusokat és az új tulajdonságokra szintén meg kell írni a mentést és visszatöltést.

Saját megvalósítás készítése

Mivel a fenti módszer használatához mindenképpen imlementálni kell a már említett interface-t előfordulhat hogy olyan oszályokat kell mentenünk amikhez nincs hozzáférésünk így kényelmetlen lenne származtatni mindegyikből egy újat csak a szükséges interface miatt. A private tulajdonságok mentésének igénye nélkül 2 extra osztály megírásával gond nélkül tudjuk menteni és visszaálíltani a lent bemutatott módszerrel is. Ehhez szükségünk lesz egy olyan osztályra ami hasonló módon fog működni mint ahogyan flash készíti el a bináris leírásból a mentett objektumokat. Szükségünk lesz egy ItemInfo nevű osztályra ami elmenti a szükséges adatokat valamint egy ClassRegister-re ami központilag fogja számon tartani az ItemInfo-k által használt alias és osztály kapcsolatokat.

data/ItemInfo.as
Komplex osztályok megadott értékeinek mentésért és újra létrehozásáért felelős osztály.

package data
{
	import flash.utils.getDefinitionByName;
	import flash.utils.getQualifiedClassName;

	public class ItemInfo
	{
		public var classNameAlias:String
		public var properties:Object

		//--------------------------------------------------------------------------
		//
		//  Constructor
		//
		//--------------------------------------------------------------------------

		public function ItemInfo(obj:Object=null, props:Object=null)
		{
			if(obj){
				classNameAlias = ClassRegister.getInfoAliasForomInstance(obj)
			}

			if(props){
				properties = props
			}else{
				properties = new Object()
			}
		}

		//--------------------------------------------------------------------------
		//
		//  Public Methods
		//
		//--------------------------------------------------------------------------

		public function addProperty(base:Object, name:String):void
		{
			properties[name] = base[name]
		}

		public function createClass():*
		{
			var clazz:Class = ClassRegister.getInfoClassFromAlias(classNameAlias)
			var factory:ClassFactory = new ClassFactory(clazz, properties)

			return factory.newInstance()
		}

		public function clone():ItemInfo
		{
			var info:ItemInfo = new ItemInfo()
			info.classNameAlias = classNameAlias

			for(var key:String in properties){
				info.properties[key] = properties[key]
			}

			return info
		}
	}
}

//--------------------------------------------------------------------------
//
//  internal ClassFactory
//
//--------------------------------------------------------------------------
internal class ClassFactory
{
	public var clazz:Class;
	public var properties:Object;

	public function ClassFactory(clazz:Class = null, properties:Object=null)
	{
		this.clazz = clazz;
		this.properties = properties
	}

	public function newInstance():*
	{
		var instance:Object = new clazz();

		if (properties != null){
			for (var p:String in properties){
				instance[p] = properties[p];
			}
		}

		return instance;
	}
}

ClassRegister.as
Központilag az ItemInfo aliasokat tároló osztály.

package
{
	import data.ItemInfo;

	import flash.net.registerClassAlias;
	import flash.text.TextField;
	import flash.utils.Dictionary;
	import flash.utils.getQualifiedClassName;

	public class ClassRegister
	{
		private static var infoClassMap:Dictionary = new Dictionary()

		public static function inititalize():void
		{
			registerClass("reg.ItemInfo", ItemInfo)

			registerInfoClass("reg.TextField", TextField)
		}

		public static function registerInfoClass(classNameAlias:String, clazz:Class):void
		{
			infoClassMap[classNameAlias] = clazz
		}

		public static function registerClass(alias:String, clazz:Class):void
		{
			registerClassAlias(alias, clazz);
		}

		public static function getInfoClassFromAlias(classNameAlias:String):Class
		{
			if(infoClassMap[classNameAlias] == null){
				throw new Error("Alias "+classNameAlias+" not found")
			}

			return infoClassMap[classNameAlias]
		}

		public static function getInfoAliasForomInstance(instance:Object):String
		{
			for(var key:String in infoClassMap){
				if(getQualifiedClassName(instance) == getQualifiedClassName(infoClassMap[key])){
					return key
				}
			}

			throw new Error("Info alias for instance "+getQualifiedClassName(instance)+" not found")
			return null
		}
	}
}

A ClassRegister központilag tárolja az inititalize() metódusában beregisztrált osztályokat. A registerClass metódusa a palayeren belül végzi el a regisztrációt a registerInfoClass pedig helyben tárolja. Mivel az ItemInfo osztályok csak az alias nevet tárolják amit később visszatöltéskor valahogy példányosítaniuk kell innen fogják lekérni hogy egy alias-hoz milyen osztály definíció lett hozzárendelve. Lehetőség lenne alias helyett az osztály teljes nevét menteni és visszakéréskor azt használni ekkor nem lenne szükség a központi alias regisztrációra de ha struktúrát változtatnák vagy osztály nevet akkor már nem lenne elérhető később. A mentés a fenti osztályok használatával a következőképpen nézne ki.

import data.ItemInfo;
import util.SaveUtil;

ClassRegister.inititalize()

private function test():void
{
	var oldField:TextField = new TextField()
	oldField.text = "This is some text"
	oldField.width = 200
	oldField.height = 20
	oldField.border = true

	var oldInfo:ItemInfo = new ItemInfo(oldField)
	oldInfo.addProperty(oldField, "text")
	oldInfo.addProperty(oldField, "width")
	oldInfo.addProperty(oldField, "height")
	oldInfo.addProperty(oldField, "border")

	var base64Str:String = SaveUtil.convertObj(oldInfo)

	var newInfo:ItemInfo = SaveUtil.createObj(base64Str)
	var newField:TextField = newInfo.createClass()

	addChild(newField)
}

A kódban először létrehozunk egy TextField-et beálíltjuk a text tulajdonságát és még pár extra értéket. Ezután készítünk egy ItemInfo-t ami elmenti pár tulajdonságát. Dekódolás után egy info objektumot kapunk ami még csak az elmentett alias nevet és a szükséges tulajdonságokat fogja tárolni. A createClass() meghívása után lekéri az alias-hoz rendelt osztály definíciót amit példányosít, majd a szintén mentett tulajdonságokat sorban beállítja rajta így megkapunk egy a leírásnak megfelelő másolatot.
Nem csak mentésere lehet használni ezt a módszert hanem objektumok klónozására is alkalmas. Az alábbi sorral kapunk egy másolatot a TextField objektumról.

var oldInfo:ItemInfo = new ItemInfo(oldField)
oldInfo.addProperty(oldField, "text")
oldInfo.addProperty(oldField, "width")
oldInfo.addProperty(oldField, "height")
oldInfo.addProperty(oldField, "border")

var copyField:TextField = oldInfo.createClass()

A mintákból lehet látni mennyire komplex adat struktúrákat lehet egyszerűen kimenteni típus vesztés nélkül. SharedObject-et használata esetén a szöveggé konvertálásra nincs is szükségünk a regisztrációs lépések után menthetünk a gépre adatokat vagy saját osztályokat így nem kell bonyolult XML struktúrák kialakításával és betöltésével bajlódnunk.

Források: SaveTypedData.zip