The ByteArray class is not as straightforward as you might think. In certain situations, it has surprising, undocumented functionality. Today’s article goes into some of these strange behaviors so you’ll get a better handle on exactly what’s going on behind the scenes.

The primary investigative tool for this article is the debug-only function flash.sampler.getSize(). Let’s first use it to establish a baseline by instantiating an empty ByteArray and checking its size with the debug version of Flash Player 13.0.0.182:

var empty:ByteArray = new ByteArray();
getSize(empty); // 160

This shows that the empty ByteArray is taking up 160 bytes of memory, mostly in the form of overhead. Now let’s write a single byte to it and check again:

empty.writeByte("A".charCodeAt(0));
getSize(empty); // 4256

Whoa! The size of the ByteArray did not simply grow by a single byte but instead by 4096 bytes (or 4 KB). If you only need to store a few bytes, that’s a lot of waste. If, however, you plan on writing a lot more bytes, the ByteArray won’t need to expand again until you’ve written your 4097th byte. Keep this behavior on mind as it’ll make a reappearance a bit later in the article.

Next, let’s try instantiating an embedded text file with only the word “TEST” in it:

[Embed(source="test.txt",mimeType="application/octet-stream")]
private static var TEST_TXT_CLASS:Class;
 
var testTxt:ByteArray = new TEST_TXT_CLASS();
getSize(testTxt); // 160

Here the ByteArray is the same 160 byte size as an empty ByteArray, but it actually has contents. The reason for this is that the ByteArray simply holds a pointer (a.k.a. reference, memory address) of the real location of the embedded file’s bytes.

Let’s see what happens when we try to change the contents of the ByteArray:

testTxt.writeByte("A".charCodeAt(0));
getSize(testTxt); // 160

The size didn’t change when just writing a single byte, unlike when we wrote to the empty ByteArray. However, the original data that the ByteArray pointed to has not been changed. This is a technique called copy-on-write that allows for many ByteArray instances to share the same data rather than storing duplicates in memory. When one needs to be written to, a copy of the original is made before the write takes place. It’s all very transparent to the user and can save a lot of memory.

So, what happens if we try to write five bytes when the length is only four?

for (var i:int = 0; i < 5; ++i)
{
	testTxt.writeByte("A".charCodeAt(0));
}
getSize(testTxt); // 4256

That fifth byte went beyond the capacity of the ByteArray to hold four bytes and caused it to resize to hold 4 KB, just like when we added the first byte to an empty ByteArray.

This leads to an observation that we can exploit to save memory. If you want a ByteArray to take up less than 4 KB of memory, you can embed a file that’s less than 4 KB in size and then write up to that much into it. Just be careful because, as above, writing more data than was in the originally-embedded file will cause the ByteArray to jump up to using 4 KB.

Finally, let’s check out the ByteArray held by LoaderInfo.bytes. This ByteArray represents the bytes of a loaded file, including the bytes of the root SWF.

getSize(loaderInfo.bytes); // 160

Again, we’ve found an empty ByteArray that’s pointing to the real contents stored elsewhere.

In summary, here’s a table showing the above results as well as the actual String values:

ByteArray Length Size Contents
empty 0 160
empty modified 1 4256 A
test.txt 4 160 TEST
test.txt modified first char 4 160 AEST
test.txt modified beyond length 5 4256 AAAAA
loaderInfo.bytes 1822 160 FWS…

Run the test app (debug Flash Player required)

And here is the full test app source code:

package
{
	import flash.display.*;
	import flash.utils.*;
	import flash.text.*;
	import flash.sampler.*;
	import flash.system.*;
 
	public class CopyOnWrite extends Sprite
	{
		[Embed(source="test.txt",mimeType="application/octet-stream")]
		private static var TEST_TXT_CLASS:Class;
 
		private var logger:TextField = new TextField();
		private function row(...cols): void
		{
			logger.appendText(cols.join(",") + "\n");
		}
 
		public function CopyOnWrite()
		{
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
 
			logger.autoSize = TextFieldAutoSize.LEFT;
			addChild(logger);
 
			if (!Capabilities.isDebugger)
			{
				row("This test app requires a debug version of Flash Player");
				return;
			}
 
			row("ByteArray", "Length", "Size", "Contents");
 
			var empty:ByteArray = new ByteArray();
			printBA("empty", empty);
 
			empty.writeByte("A".charCodeAt(0));
			printBA("empty modified", empty);
 
			var testTxt:ByteArray = new TEST_TXT_CLASS();
			printBA("test.txt", testTxt);
 
			testTxt.writeByte("A".charCodeAt(0));
			printBA("test.txt modified first char", testTxt);
 
			for (var i:int = 0; i < 5; ++i)
			{
				testTxt.writeByte("A".charCodeAt(0));
			}
			printBA("test.txt modified beyond length", testTxt);
 
			printBA("loaderInfo.bytes", loaderInfo.bytes);
		}
 
		private function printBA(lbl:String, ba:ByteArray): void
		{
			ba.position = 0;
			row(lbl, ba.length, getSize(ba), ba.readUTFBytes(ba.length));
			ba.position = 0;
		}
	}
}

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