ByteArray Secrets
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!
#1 by Antoine on April 15th, 2014 ·
Hi, very interesting read!
I had always assumed that embedding bytes as a class and instantiate them would take twice the memory !
Do you know if the same applies to images embedded and then instantiated as Bitmap instances ?
#2 by jackson on April 15th, 2014 ·
Good question. I’ll check it out.
#3 by Gordon on June 18th, 2014 ·
hi, good post.
I use your demo, it runs good, but when i change the code to use the function
writeBytes(bytes:ByteArray, offset:uint = 0, length:uint = 0):void
the size will be 4272.
do you know this issue? and how to fix it?
thanks
#4 by jackson on June 18th, 2014 ·
I’m not sure what you’re asking. Can you post more of the code, explain what you expect to see, and what you’re actually seeing?
#5 by Gordon on June 18th, 2014 ·
your code
i change to
you can see the print is diffrent
#6 by jackson on June 20th, 2014 ·
I tried this out and get the same result for both versions: