суббота, 27 ноября 2010 г.

Первые биты

Представляю вашему вниманию перевод главы «Chapter 1 - The first bits» из несуществующей пока книги «What can you do with bytes ?»


Первые биты



Flash Player 9 открыл новые возможности для флеш-разработчиков, позволяя им управлять данными на низком уровне. В этой главе мы рассмотрим основы двоичных данных и посмотрим, как использовать эти знания в Flash Player с помощью класса ByteArray. Каждое понятие будет рассмотрено для различных случаев использования. После прочтения этой главы вы будете знать все необходимое, чтобы понять спецификацию любого типа файла и создавать парсеры и генераторы этих файлов.

Двоичное кодирование



Для того чтобы понять причину использования двоичных данных нам нужно вернуться к истокам компьютерных вычислений. Самый низкий уровень ввода, который может понять компьютер, представлен числами 0 и 1, которые представляют состояния электрической цепи:

- 0: цепь разомкнута.
- 1: цепь замкнута.

Процессоры понимают только двоичные данные. Хотя это может показаться сложным на первый взгляд, но это просто вид представления данных. Перед тем, как сосредоточиться на классе ByteArray, нам нужно ознакомиться с концепцией систем счисления. Человек в основном использует в качестве основания для всех чисел число 10, такая система счисления называется десятичной. Для представления времени мы используем шестидесятеричную систему счисления, базирующуюся на 60 символах. Таким образом, когда 60 секунд истекли, мы не пишем 61-ю секунду, а добавляем новое значение, которое обозначает минуту и начинаем отсчет секунд с нуля.
В повседневности мы используем следующие символы для представления чисел:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9


После 9 для получения нового числа мы должны использовать комбинации предыдущих символов. Для этого мы вводим новое значение, и опять начинаем счет с нуля. Каждый раз, когда мы двигаемся влево, мы добавляем степень к основанию 10. На рисунке 1 представлено разделение числа на группы с различными степенями числа 10.

Рисунок 1


Рисунок 1




Наша десятичная система работает со степенями основания 10 и мы можем записать число 750 в развернутой форме (здесь ^ - это символ возведения в степень):

7*100 + 5*10 = 7*10^2 + 5*10^1


В отличие от десятичной системы, двоичная система позволяет использовать только символы 0 и 1. Число 150 в двоичной системе может быть записано как

10010110


Мы узнаем, как преобразовывать представления числа из двоичного вида в десятичный вид. На сей раз, мы применяем тот же метод разбивки числа, но используем в качестве основы не степени числа 10, а степени числа 2. На рисунке 2 представлена последовательность двоек в определенной степени. Эта степень определяется позицией (разрядом) цифры в числе.

Рисунок 2


Рисунок 2



В школе нас учили записывать число 10 в десятичном представлении. Если бы стандартом было использование двоичной системы счисления, то левая колонка вызывала бы такое же недоумение, как сейчас правая колонка.

Таблица 1. – Различие между десятичной и двоичной системами счисления.

Десятичная система счисления Двоичная система счисления
0 0
1 1
2 10
3 11
4 100
5 101
6 110
7 111
8 1000
9 1001
10 1010

Для понимания двоичной системы счисления нам нужно знать все о битах, как только это знание будет, мы введем понятие байта.


Позиция и вес бита



Вернемся к числу 150, которое мы записали в двоичном виде:

10010110


Один двоичный разряд в двоичной системе называются битом (это определение просто понять, если вы прочитали статьи про разряд и про двоичную систему на вики), от английского BInary digiT. В отличие от десятичной системы, которая работает со степенями основания 10, двоичная система работает со степенями основания 2. Каждое перемещение бита из одной позиции в другую изменяет оригинальное значение всего числа в зависимости от позиции бита. Принимая во внимание, что позиция бита означает его вес (старшинство), старший бит будет всегда располагаться в крайнем левом положении (номер его позиции больше), в отличие от младшего бита, который располагается в крайней правой позиции.

На рисунке 3 выделен младший бит

Рисунок 3


Рисунок 3



На рисунке 4 выделен старший бит

Рисунок 4


Рисунок 4



В результате, изменение значения одного бита изменяет все число в зависимости от текущей позиции этого самого бита. В случае числа 150, мы видим, что если мы изменим старший бит в 0, то мы вычтем число 128 (2^7) из числа 150 (см. рисунок 5):

Рисунок 5


Рисунок 5



Наоборот, если мы изменим бит с меньшим разрядом, то начальное число изменится менее существенно, в этом случае вычитается число 4 (2^2):

Рисунок 6


Рисунок 6



В результате, для преобразования числа из двоичной системы счисления в десятичную, нужно сделать следующее:
берем число 150 в двоичном виде:

10010110


Нам нужно умножить значение каждого бита на основание, в степени разряда этого бита, и сложить результат:

1*2^7 + 0*2^6 + 0*2^5 + 1*2^4 + 0*2^3 + 1*2^2 + 1*2^1 + 0*2^0


Это даст нам более компактное выражение:

2^7 + 2^4 + 2^2 + 2^1 = 150


Теперь, быстро переведем следующее число в десятичную систему

1000


Должно получиться 8. Получилось?

Теперь другой, более простой пример:

101


Это даст 5, возможно вы уже заметили, что в двоичном представлении не каждый младший бит принимает значение 1. Мы вернемся к этому позже и посмотрим, как определять нечетные числа с помощью побитовых операторов.

Мы часто находим понятие битов в цветах. Позже мы увидим, что цвета в основном сохраняются в 8, 16 и 32 битов. Есть еще такая остроумная шутка (см. рисунок 7) – есть только 10 типа людей в мире, те которые понимают двоичную систему счисления и те которые ее не понимают. Мы знаем, что 10 в двоичном представлении означает 1*2^1, что дает нам число 2 в десятичной системе.


Рисунок 7


Рисунок 7



В отличие от двоичной или десятеричной системы, шестнадцатеричная система использует 16 символов от 0 до F для записи числа. Теперь мы работаем со степенями основания 16 (см рисунок 8)


Рисунок 8


Рисунок 8



В таблице 2 представлены все символы, использующиеся для записи в шестнадцатеричной системе счисления.

Шестнадцатеричная система счисления Десятичная система счисления
0 0
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
A 10
B 11
C 12
D 13
E 14
F 15

Из шестнадцатеричного представления значение 1A может быть переведено в десятичное следующим образом:

1*16^1 + 10*16^0 = 26


Помните, что по умолчанию Flash Player выводит значения, используя десятичную систему счисления. Однако когда работаешь с двоичными данными, то удобнее выводить в двоичной системе. Вместо того чтобы делать преобразования вручную, мы можем положиться на метод toString() класса Number. Вот его сигнатура:

toString(radix:*=10):String


Параметр radix определяет основание системы счисления, в которую необходимо преобразовать число. В следующем коде мы конвертируем число 5 из десятичной системы в двоичную:

var value:int = 5;
// outputs : 101
trace (value.toString(2));


Если мы начнем преобразовывать числа от 1 до 10 из десятичной системы в двоичную, то получим значения из таблицы 1:

var zero:int = 0;
var one:int = 1;
var two:int = 2;
var three:int = 3;
var four:int = 4;
var five:int = 5;
var six:int = 6;
var seven:int = 7;
var eight:int = 8;
var nine:int = 9;
var ten:int = 10;
// outputs : 0
trace(zero.toString(2));
// outputs : 1
trace(one.toString(2));
// outputs : 10
trace(two.toString(2));
// outputs : 11
trace(three.toString(2));
// outputs : 100
trace(four.toString(2));
// outputs : 101
trace(five.toString(2));
// outputs : 110
trace(six.toString(2));
// outputs : 111
trace(seven.toString(2));
// outputs : 1000
trace(eight.toString(2));
// outputs : 1001
trace(nine.toString(2));
// outputs : 1010
trace(ten.toString(2));


Точно так же мы можем преобразовать числа из десятичной системы в шестнадцатеричную:

var zero:int = 0;
var one:int = 1;
var two:int = 2;
var three:int = 3;
var four:int = 4;
var five:int = 5;
var six:int = 6;
var seven:int = 7;
var eight:int = 8;
var nine:int = 9;
var ten:int = 10;
var eleven:int = 11;
var twelve:int = 12;
var thirteen:int = 13;
var fourteen:int = 14;
var fifteen:int = 15;
// outputs : 0
trace(zero.toString(16));
// outputs : 1
trace(one.toString(16));
// outputs : 2
trace(two.toString(16));
// outputs : 3
trace(three.toString(16));
// outputs : 4
trace(four.toString(16));
// outputs : 5
trace(five.toString(16));
// outputs : 6
trace(six.toString(16));
// outputs : 7
trace(seven.toString(16));
// outputs : 8
trace(eight.toString(16));
// outputs : 9
trace(nine.toString(16));
// outputs : a
trace(ten.toString(16));
// outputs : b
trace(eleven.toString(16));
// outputs : c
trace(twelve.toString(16));
// outputs : d
trace(thirteen.toString(16));
// outputs : e
trace(fourteen.toString(16));
// outputs : f
trace(fifteen.toString(16));


Пока мы просто говорили о битах, но как нам хранить определенные выражения? Для хранения и обработки бит они объединяются в пакеты, таким образом, родилось понятие байта, о котором мы сейчас поговорим.


Что такое байт?



Понятие байт введено для выражения количества данных. Мы используем его каждый день для выражения веса файла. Конечно, мы не говорим, что файл MP3 весит 3 145 728 байт, мы просто говорим 3 мегабайта.

Кратные байту единицы информации вводятся следующим образом (тут надо не запутаться и прочитать статью на вики):

- 1 килобайт (kb) = 10^3 байт = 1000 байт;
- 1 мегабайт (mb) = 10^6 байт = 1000 килобайт (1 000 000);

Не путайте понятия бит и байт. Байт состоит из 8 бит:

 1 0 1 1 0 0 0 0 byte
|_ _ _ _ _ _ _ _|
7 6 5 4 3 2 1 0 bits


Возможно, вы уже заметили, что индексация разряда бита начинается с нуля и поскольку байт содержит 8 бит, то он не может хранить число, большее чем 255 (беззнаковый байт). Для преобразования полного байта (1111 1111) в десятичное значение мы используем методику, описанную ранее:

1*2^7 + 1*2^6 + 1*2^5 + 1*2^4 + 1*2^3 + 1*2^2 + 1*2^1 + 1*2^0


Это даст нам следующее:

128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255;


Как мы увидим далее, в байте возможно хранение значений от -128 до 127 используя знаковый байт (отрицательный). Вы спросите, почему максимальное значение 127?
В случае отрицательного значения, старший бит будет использоваться для хранения знака и в результате для хранения значения будут доступны только 7 бит.

1*2^6 + 1*2^5 + 1*2^4 + 1*2^3 + 1*2^2 + 1*2^1 + 1*2^0


Что даст нам следующее:

64 + 32 + 16 + 8 + 4 + 2 + 1 = 127


Стоит отметить, что при работе с двоичными данными, часто используется шестнадцатеричная система счисления и соответствующие редакторы (hex-редакторы) для более удобного представления байта. Выберите себе редактор и сделайте его лучшим своим другом при работе с двоичными данными.
Здесь список наиболее распространенных hex-редакторов:

• Free Hex Editor Neo (free): http://www.hhdsoftware.com/Family/hex-editor.html ;
• HxD (free): http://mh-nexus.de/hxd ;
• Hex Workshop: http://www.hexworkshop.com ;
• Hexprobe: http://www.hexprobe.com/hexprobe.

На практике, гораздо удобнее читать значение байта в шестнадцатеричном представлении:

4E


чем в двоичном

1001110


Число 255 хранится в беззнаковом байте, может выражаться двумя символами благодаря шестнадцатеричной системе счисления

FF


Чтобы закрепить это все на практике, откроем файл в формате PNG в любом hex-редакторе.
На рисунке 9 представлены байты изображения в шестнадцатеричном представлении:


Рисунок 9


Рисунок 9



Мы видим, что каждая колонка состоит из двух символов представляющих значение, хранящееся в одном байте, в шестнадцатеричном представлении.
Как мы увидим позже, каждый файл может быть идентифицирован его заголовком. Следовательно, если мы прочитаем спецификацию PNG, мы увидим, что любой правильный файл этого формата должен начинаться с подписи, которая состоит из следующих десятичных значений:

137
80
78
71
13
10
26
10


Для конвертации в шестнадцатеричное представление каждого из этих значений пишем следующий код:

var firstByte:int = 137;
var secondByte:int = 80;
var thirdByte:int = 78;
var fourthByte:int = 71;
var fifthByte:int = 13;
var sixthByte:int = 10;
var seventhByte:int = 26;
var eighthByte:int = 10;
// outputs : 89
trace(firstByte.toString(16));
// outputs : 50
trace(secondByte.toString(16));
// outputs : 4e
trace(thirdByte.toString(16));
// outputs : 47
trace(fourthByte.toString(16));
// outputs : d
trace(fifthByte.toString(16));
// outputs : a
trace(sixthByte.toString(16));
// outputs : 1a
trace(seventhByte.toString(16));
// outputs : a
trace(eighthByte.toString(16));


На рисунке 10 показан подсвеченный заголовок PNG файла:


Рисунок 10


Рисунок 10




Порядок байтов



Когда используется множество байт для хранения значения, важно обращать внимание на порядок, в котором они следуют друг за другом. Это понятие называют порядком байтов - метод записи байтов многобайтовых чисел.

Предположим, что нам нужно сохранить следующее значение:

550000000


Это 32-х битное число, для хранения которого потребуется 32 бита:

00100000110010000101010110000000


Мы можем сгруппировать эту последовательность в 4 байта:

00100000 11001000 01010101 10000000


Теперь мы можем представить все в шестнадцатеричном представлении:

20 C8 55 80


Процессоры, такие как Motorolla 68000 и SPARC используемые Sun Microsystems, используют тип хранения, называемый от старшего к младшему (big-endian – дословно: «тупоконечный»). В этом случае в крайней левой позиции находится старший байт (в нашем случае байт со значением 20 в шестнадцатеричном представлении). Другие процессоры, такие как 5602 Motorola или Intel x86, используют тип хранения, называемый от младшего к старшему (little-endian – дословно: «остроконечный») и хранят байты в противоположном порядке (см таблицу 3).

Таблица 3. – Различия между порядками от старшего к младшему и от младшего к старшему

От старшего к младшему (big-endian)
0 1 2 3
20(старший байт) C8 55 80
От младшего к старшему (little-endian)
0 1 2 3
80 55 C8 20(старший байт)

Разрабатывая приложения на ActionScript3 и используя класс ByteArray, мы должны помнить всегда о порядке байтов, особенно в контексте внешних коммуникаций.


Побитовые операторы



Чтобы управлять каждым битом в байте мы будем использовать побитовые операторы. Список их представлен ниже.

& bitwise AND Преобразует выражения expression1 и expression2 в 32-разрядные целые числа без знака и выполняет логическую операцию AND для каждого бита целочисленных параметров.
<< bitwise left shift Преобразует выражение expression1 и shiftCount в 32-разрядные целые числа и сдвигает все биты в выражении expression1 влево на число позиций, заданное целым числом, полученным в результате преобразования выражения shiftCount.
~ bitwise NOT Преобразует expression в 32-разрядное целое число со знаком, затем применяет побитовое дополнение до единицы.
| bitwise OR Преобразует expression1 и expression2 в 32-разрядные целые числа без знака и ставит 1 в позиции каждого бита, где соответствующие биты в expression1 или expression2 являются 1.
>> bitwise right shift Преобразует выражения expression и shiftCount в 32-разрядные целые числа и сдвигает все биты в выражении expression вправо на число позиций, заданное целым числом, полученным в результате преобразования выражения shiftCount.
>>> bitwise unsigned right shift Аналогичен оператору побитового сдвига вправо (>>) за исключением того, что он не сохраняет знак исходного выражения, поскольку биты слева всегда заполняются нулями (0).
^ bitwise XOR Преобразует expression1 и expression2 в 32-разрядные целые числа без знака и ставит 1 в позиции каждого бита, где соответствующие биты в expression1 или expression2 (но не в обоих выражениях) равны 1.

Не беспокойтесь, мы не будем использовать все эти операторы, только некоторые из них будут полезны в повседневной разработке, но понимать нужно каждый из них. Наиболее важные операторы для понимания это операторы побитового сдвига >>, >>> и <<.

Имейте в виду, что использование любого из этих операторов будет преобразовывать значение в целое число со знаком или без него. Любое число с плавающей запятой будет преобразовано в целое число.

// a decimal value
var value:Number = 67.4;
// outputs : 1
trace(value & 1);
//outputs : 134
trace(value << 1);
// outputs : -68
trace(~value);
// outputs : 67
trace(value | 1);
// outputs : 33
trace(value >> 1);
// outputs : 33
trace(value >>> 1);
// outputs : 66
trace(value ^ 1);


Теперь рассмотрим побитовые операторы сдвига, работу который действительно очень просто понять. Перемещая биты влево или вправо, мы производим изменение на сдвинутое значение. Каждое перемещение бита эквивалентно делению или умножению на число 2 в степени, которая равняются разряду бита.

Для понимания следующего механизма нужно временно забыть десятичную систему и думать о двоичной. Итак, возьмем число 8 в двоичном представлении:

1000


Если мы начнем сдвигать биты вправо, то не будет доступного свободного пространства. В результате, сдвиг всех битов на одну позицию влево даст нам значение 4 (100 в двоичном представлении)

1000
0100 >> 1
---------
100


Такой способ очень полезен для повышения производительности операции деления или умножения на 2 в определенной степени, каждый сдвиг на n позиций эквивалентен делению или умножению на 2^n.

expression >> shiftCount

Для перевода этого в ActionScript возьмем следующий код:

var size:int = 8;
// division by 21
var result:int = size >> 1;
// outputs : 4
trace(result);


В предыдущем случае, мы сдвинули все биты на одну позицию вправо, т.е. просто разделили на 2^1. Перемещая биты на две позиции мы разделим на 2^2.

var size:int = 8;
// division by 2^2
var result:int = size >> 2;
// outputs : 2
trace( result );


Перемещение битов в противоположном направлении эквивалентно умножению значения на 2 в степени, равной количеству позиций, на которые происходит перемещение.

var size:int = 8;
// multiply by 2^2
var result:int = size << 2;
// outputs : 32
trace( result );


Перемещая биты влево, мы производим умножение:

1000
100000 << 2
-----------
100000


Для повышения производительности более выгодно использовать именно эти способы деления и умножения. В следующем коде мы используем традиционные операторы:

var started:Number = getTimer();
var value:int = 8;
var result:int;
for (var i:int = 0; i< 5000000; i++)
result = value / 4;
// outputs : 2
trace(result);
//outputs : 617
trace(getTimer() - started);


Теперь используем побитовые операторы, для деления нужно использовать побитовый оператор сдвига вправо:

var started:Number = getTimer();
var value:int = 8;
var result:int;
for (var i:int = 0; i< 5000000; i++)
result = value >> 2;
// outputs : 2
trace(result);
//outputs : 586
trace(getTimer() - started);


Мы видим очень небольшое различие в производительности. Конечно, эти побитовые операторы не используются только для деления или умножения. Мы вернемся к этому позже.

Для того чтобы изменить значение одного бита мы используем оператор |(OR).

expression1 | expression2

В результате, если хоть один бит из двух сравниваемых будет равен 1, то результирующий бит также будет равен 1.

   0110
OR 0011
----
0111


Для более легкого переключения значения битов мы используем оператор OR. В следующем коде мы изменяем значение старшего седьмого бита в 1:

// byte : 127
var byte:int = 0x7F;
// set the 7th bit on the left (msb)
// outputs : 255
trace(byte | 1<<7);


Отметим, что мы просто использовали оператор << для создания нового двоичного числа, которое послужило нам в качестве битовой маски для операции побитового ИЛИ. Для лучшего понимания ниже представлен этот процесс с двоичными числами.

  01111111
| 10000000 1 << 7
--------
11111111


Аналогично, мы можем использовать оператор ^ (XOR), который производит операцию побитового исключающего ИЛИ для битов двух выражений одинаковой длины:

expression1 ^ expression2

В результате мы получим 1 в той позиции, где значения битов этих выражений различны и 0 там, где они одинаковы. Это иллюстрируется следующим примером:

    1101
XOR 1011
----
0110


Теперь разберемся с оператором & (AND), который выполняет операцию побитового И для битов двух выражений одинаковой длины:

expression1 & expression2

В результате мы получим 1 в той позиции, где значения битов этих выражений равны 1:

    1101
AND 1011
----
1001


Этот оператор можно использовать для тестирования значения каждого бита в байте. Пример такой проверки представлен ниже:

// byte : 255
var byte:int = 0xFF;
// testing of each bit from the byte
// outputs : true
trace((byte & (1<<7)) != 0);
// outputs : true
trace((byte & (1<<6)) != 0);
// outputs : true
trace((byte & (1<<5)) != 0);
// outputs : true
trace((byte & (1<<4)) != 0);
// outputs : true
trace((byte & (1<<3)) != 0);
// outputs : true
trace((byte & (1<<2)) != 0);
// outputs : true
trace((byte & (1<<1)) != 0);
// outputs : true
trace((byte & 1) != 0);


Тоже самое, мы берем значение 1 и сдвигая биты влево, получаем двоичное значение:

1
1000000 1 << 7
-------
1000000


Заметим, что опять мы используем оператор побитового сдвига влево для создания маски для определения состояния каждого бита. Маска нужна для создания байта, в котором сохранен только тот бит, который мы хотим проверить.
Теперь у нас есть маска для проверки значения старшего бита (msb):

  11111111
& 10000000
--------
10000000


В следующем примере мы видим, что с помощью маски мы сохраняем значение 1 только для бита в той позиции, в которой биты равны 1 в обоих выражениях:

  01010100
& 01000000
--------
01000000


В этом случае, значения битов в позиции, заданной в маске не совпадают и поэтому все биты принимают значение 0:

  00100000
& 01000000
--------
00000000


Для простой записи любого бита мы можем создать простые методы writeBits() и readBites():

var buffer:int = 0;
var inc:int = 0;
var incRead:int = 0;

function writeBit (pBit:uint):uint
{
return buffer |= (pBit << inc++);
}

function readBit ():uint
{
return uint((buffer & (1 << incRead++)) != 0);
}

// outputs : 1
trace(writeBit(1).toString(2));
// outputs : 11
trace(writeBit(1).toString(2));
// outputs : 111
trace(writeBit(1).toString(2));
// outputs : 111
trace(writeBit(0).toString(2));
// outputs : 10111
trace(writeBit(1).toString(2));


Эти методы мы можем переместить в специальный класс BitWriter:

package org.bytearray.utils
{
public class BitWriter
{
private var buffer:uint = 0;
private var length:uint = 0;
private var pointer:uint = 0;

public function writeBit(pBit:int):uint
{
if(0 > pBit || 1 < pBit) throw new Error("Argument should be 0 or 1");
return buffer |= (pBit << (length = pointer++));
}

public function readBit():uint
{
if (pointer > length) throw new Error ("End of data encountered.");
return uint((buffer & (1 << pointer++)) != 0);
}

public function set position (pPosition:int):void
{
pointer = pPosition;
}

public function get position ():int
{
return pointer;
}
}
}


Теперь, когда нам необходимо изменить значения битов, мы пишем подобные строки:

import org.bytearray.utils.BitWriter;
var writer:BitWriter = new BitWriter();
// outputs : 1
trace(writer.writeBit(1).toString(2));
// outputs : 11
trace(writer.writeBit(1).toString(2));
// outputs : 111
trace(writer.writeBit(1).toString(2));
// outputs : 111
trace(writer.writeBit(0).toString(2));
// outputs : 10111
trace(writer.writeBit(1).toString(2));


Чтобы читать значения битов, мы используем метод readBit():

import org.bytearray.utils.BitWriter;
var writer:BitWriter = new BitWriter();
// outputs : 1
trace(writer.writeBit(1).toString(2));
// outputs : 11
trace(writer.writeBit(1).toString(2));
// outputs : 111
trace(writer.writeBit(1).toString(2));
// outputs : 111
trace(writer.writeBit(0).toString(2));
// outputs : 10111
trace(writer.writeBit(1).toString(2));
// reset position
writer.position = 0;
// outputs : 1
trace(writer.readBit());
// outputs : 1
trace(writer.readBit());
// outputs : 1
trace(writer.readBit());
// outputs : 0
trace(writer.readBit());
// outputs : 1
trace(writer.readBit());


Хранение состояний кнопки в байте, например, может быть очень полезным для экземпляров класса контроллера. В WiiFlash (wiiflash.bytearray.org), каждое состояние кнопки Wiimote записано в отдельном бите. 0 значит, что кнопка не нажата, а 1 – кнопка начата. С помощью этой техники мы можем хранить все состояния кнопок всего в нескольких битах. Здесь часть кода C# сервера WiiFlash.

Возможно, вы заметили, что побитовые операторы в C# используются и действуют так же, как и во многих других языках программирования:

// create the ByteArray
MemoryStream ms = new MemoryStream(buffer);
// create a byte variable to store the states
byte buttonStateHI = 0;
// create a byte variable to store the states
byte buttonStateLO = 0;
if (ws.ButtonState.One)
buttonStateHI |= 1 << 7;
if (ws.ButtonState.Two)
buttonStateHI |= 1 << 6;
if (ws.ButtonState.A)
buttonStateHI |= 1 << 5;
if (ws.ButtonState.B)
buttonStateHI |= 1 << 4;
if (ws.ButtonState.Plus)
buttonStateHI |= 1 << 3;
if (ws.ButtonState.Minus)
buttonStateHI |= 1 << 2;
if (ws.ButtonState.Home)
buttonStateHI |= 1 << 1;
if (ws.ButtonState.Up)
buttonStateHI |= 1;
if (ws.ButtonState.Down)
buttonStateLO |= 1 << 7;
if (ws.ButtonState.Right)
buttonStateLO |= 1 << 6;
if (ws.ButtonState.Left)
buttonStateLO |= 1 << 5;


Оператор побитового И также обычно используется, чтобы проверить, является ли значение четным или нечетным. Как мы сказали раньше, нечетное число всегда имеет младший бит со значением 1. Проверить значение бита мы можем простейшей операцией с маской:

// in binary : 100
var value:int = 4;
// checking of the least significant bit (lsb)
// outputs : 0
trace(value & 1);
// in binary : 101
value = 5;
// checking of the least significant bit (lsb)
// outputs : 1
trace(value & 1);


Метод isEven возвращает true если значение четное и false если значение нечетное:

// in binary: 100
var value:int = 4;
// checking of the least significant bit (lsb)
// outputs : true
trace(isEven (value));
// in binary : 101
value = 5;
// checking of the least significant bit (lsb)
// outputs : false
trace(isEven (value));

function isEven (pValue:Number):Boolean
{
return ((pValue & 1) == 0);
}


Оператор побитового И (&) можно использовать для ограничения числа в двоичном представлении каким-либо максимальным значением. Для этого нам необходимо определить, сколько регистров занимает это максимальное значение. Допустим, мы хотим ограничиться восемью регистрами, чтобы значение могло храниться в байте.
В байте, если мы добавим 1 к 0xFF, полученное значение будет равно нулю

1 0 0 0 0 0 0 0 0
|_ _ _ _ _ _ _ _|
8 7 6 5 4 3 2 1 0


Так как байт не может содержать в себе 9 бит, то сохраняются только 8 младших битов:

00000000


Или проще:

0


Красиво правда? Но у нас нет типов данных, которые бы описывали это свойство байта, для чисел мы используем типы int и uint, а они 32-х битные.
Если мы будем использовать такие типы, то добавление единицы к числу 0xFF даст число 256, которое поломает всю эмуляцию работы ЦПУ :)

100000000


Благодаря оператору побитового И мы можем всегда удостовериться, что сохраняем только 8 младших битов. Ниже основная идея:

  100000000 Register
& 11111111 Mask
---------
00000000


Которая выражается в коде следующим образом:

// byte : 256
var byte:int = 0x100;
// masking, only the 8 (lsb) bits are preserved
// outputs : 0
trace(byte & 0xFF);


Вообще, оператор & может использоваться для выделения компонентов цвета. Цвета обычно представлены 24-х и 32-х битными целыми числами, где на каждый канал (красный, зеленый и синий) выделяется по 8 бит (концепцию «8 бит на компонент» можно увидеть в спецификациях различных файлов).
В следующем коде мы берем случайный 24-х битных цвет и выделяем с помощью битовой маски каждый канал:

// color 24 bits
var color:uint = 0xFF9911;
// extraction of the red component
trace(color & 0xFF0000);
// extraction of the green component
trace(color & 0x00FF00);
// extraction of the blue component
trace(color & 0x0000FF);


В этом коде мы просто использовали оператор побитового И для выделения битов с помощью битовой маски. Для того чтобы проще было понять этот первый шаг, посмотрим на значения цветов в двоичном представлении:

11111111 10011001 00010001
RED GREEEN BLUE


Здесь мы визуально выделили каждый байт (8 бит), теперь мы можем отфильтровать и вывести каждый канал с помощью оператора побитового И и битовой маски.
Выделим красный канал:

  11111111 10011001 00010001 Color
& 11111111 00000000 00000000 Mask
--------------------------
11111111 00000000 00000000


Теперь нам нужно сдвинуть 8 бит красного канала на 16 позиций вправо, чтобы получить окончательное значение:

// color 24 bits
var color:uint = 0xFF9911;
// extraction of the red component
// outputs : 255
trace((color & 0xFF0000) >> 16);


Для зеленого имеем следующее:

  11111111 10011001 00010001 Color
& 00000000 11111111 00000000 Mask
--------------------------
00000000 10011001 00000000


Теперь, чтобы получить окончательный результат, мы сдвигаем биты зеленого канала вправо всего на 8 позиций:

// 24 bits color
var color:uint = 0xFF9911;
// extraction of the red component
// outputs : 255
trace((color & 0xFF0000) >> 16);
// extraction of the green component
// outputs : 153
trace((color & 0x00FF00) >> 8);


Для синего канала операцию сдвига производить не нужно, т.к. биты этого канала и так стоят в крайней правой позиции:

  01011001 00111111 10101100 Color
& 00000000 00000000 11111111 Mask
--------------------------
00000000 00000000 10101100


Выводим окончательный результат:

// 24 bits color
var color:uint = 0xFF9911;
// extraction of the red component
// outputs : 255
trace((color & 0xFF0000) >> 16);
// extraction of the green component
// outputs : 153
trace((color & 0x00FF00) >> 8);
// extraction of the blue component
// outputs : 17
trace(color & 0x0000FF);


С помощью метода toString() мы конвертируем числа из десятичного представления в шестнадцатеричное:

// 24 bits color
var color:uint = 0xFF9911;
// extraction of the red component
// outputs : ff
trace(((color & 0xFF0000) >> 16).toString(16));
// extraction of the green component
// outputs : 99
trace(((color & 0x00FF00) >> 8).toString(16));
// extraction of the blue component
// outputs : 11
trace((color & 0x0000FF).toString(16));


Итак, мы только что извлекли значения всех наших цветовых каналов. Теперь посмотрим, как работать с 32-х битными значениями цвета.
В следующем коде мы пытаемся выделить значение альфа-канала и за 32-х битного значения цвета:

// 32-bit color
var color:uint = 0xCCFF9911;
// extraction of the alpha component
// outputs : -34
trace(((color & 0xFF000000) >> 24).toString(16));
// extraction of the red component
// outputs : ff
trace(((color & 0xFF0000) >> 16).toString(16));
// extraction of the green component
// outputs : 99
trace(((color & 0x00FF00) >> 8).toString(16));
// extraction of the blue component
// outputs : 11
trace((color & 0x0000FF).toString(16));


Как видите, значение -34 не соответствует значению альфа-канала 204 (0хСС). Проблема в том, что мы не можем использовать знаковый сдвиг битов на 24 позиции вправо, т.к. мы работаем с 32-х битными числами, поэтому нам нужно использовать оператор побитового беззнакового сдвига вправо (>>>):

// 32 bits color
var color:uint = 0xCCFF9911;
// extraction of the alpha component
// outputs : cc
trace(((color & 0xFF000000) >>> 24).toString(16));
// extraction of the red component
// outputs : ff
trace(((color & 0xFF0000) >> 16).toString(16));
// extraction of the green component
// outputs : 99
trace(((color & 0x00FF00) >> 8).toString(16));
// extraction of the blue component
// outputs : 11
trace((color & 0x0000FF).toString(16));


Теперь, мы можем использовать оператор побитового ИЛИ | для создания цвета. Мы помним, что использовали этот оператор для проверки значения бита. В следующем коде мы создаем 32-х битный цвет из четырех компонентов (альфа, красный, зеленый и синий):

var transparency:int = 0xFF;
var red:int = 0x88;
var green:int = 0x7E;
var blue:int = 0x11;
var color:uint = transparency << 24 | red << 16 | green << 8 | blue;
// outputs : FF887E11
trace (color.toString(16).toUpperCase());


Для лучшего понимания представим всю эту операцию в следующем виде:

11111111 00000000 00000000 00000000 <- Transparency (shift 24 bits on the left)
OR 10001000 00000000 00000000 <- Red (shift 16 bits on the left)
OR 01111110 00000000 <- Green (shift 8 bits on the left)
OR 00010001 <- Blue (no shift required)
---------------------------------------------------
11111111 10001000 01111110 00010001 (base 2)
---------------------------------------------------
0xFF 0x88 0X7E 0x11 (base 16)


Если нам нужно изменить только один компонент (байт) из всего 32-х битного цвета, мы используем оператор побитового ИЛИ вместе с простейшей маской, которая содержит нужный байт:

// color 32 bits
var color:uint = 0xFFAA9911;
color |= 0x00BB0000;
// outputs : ffbb9911
trace(color.toString(16));


Эта же операция в двоичном представлении

  11111111 10101010 10001001 00010001 Color
| 00000000 10111011 00000000 00000000 Red Mask
-------------------------- --------
11111111 10111011 10001001 00010001


Теперь, если нужно присвоить цветовому каналу значение 0, то мы не сможем ограничиться одним оператором, также нам придется использовать оператор побитового НЕ (~).

~expression

Оператор побитового НЕ инвертирует значения всех битов:

NOT 1011
----
0100


Поскольку мы не можем сдвигать нули, мы сдвинем наши биты влево на 16 разрядов и, инвертируя, получим следующую маску:

11111111 00000000 11111111 11111111


Благодаря этой маске и оператору побитового НЕ мы обнуляем значение красного канала:

  11111111 10101010 10001001 00010001 Color
& 11111111 00000000 11111111 11111111 Red Mask
-------------------------- --------
11111111 00000000 10001001 00010001


Эта операция иллюстрирована следующим кодом:

var color:uint = 0xFFAA9911;
color = color & ~(0xFF<<16);
// outputs : ff009911
trace(color.toString(16));


Чтобы сделать код более компактным, используем оператор &=:

// color 32 bits
var color:uint = 0xFFAA9911;
color &= ~(0xFF<<16);
// outputs : ff009911
trace(color.toString(16));


К этому оператору мы вернемся очень скоро. Заметим что при выводе с помощью trace() не показываются нули, которыми должны заполниться биты слева.

var color:uint = 0xFF<<8;
// outputs : 1111111100000000
trace(color.toString(2));


Когда мы работаем с масками и двоичными данными мы можем использовать небольшой пользовательский метод, для того чтобы видеть какие биты из 32-х битного участка памяти доступны для сдвига в них.

function convertToBinary(numberToConvert:Number):String
{
var result:String = "";
var lsb:Number;
const lng :int = 32;

for (var i:int = 0; i < lng; i++)
{
// Extract least significant bit using bitwise AND.
lsb = numberToConvert & 1;
// Add this bit to the result.
result = (lsb == 1 ? "1" : "0") + result;
// Shift numberToConvert right by one bit, to see next bit.
numberToConvert >>= 1;
}

return result;
}

var color:uint = (0xFF<<8);
// outputs : 00000000000000001111111100000000
trace(convertToBinary(color));


Может использоваться число в десятичном представлении:

var value:int = 128;
// outputs : 00000000000000000000000010000000
trace(convertToBinary(value));


Битовые операторы используют знак = и позволяют присваивать результат операции сразу определенной переменной. Вместо того чтобы писать так:

var size:int = 8;
// division by 2exp1
size = size >> 1;
// outputs : 4
trace(size);


Мы пишем так:

var size:int = 8;
// division by 2exp1
size >>= 1;
// outputs : 4
trace(size);


И вместо того, чтобы писать так:

var size:int = 0x100;
size = size & 0xFF;
// outputs : 0
trace(size.toString(16));


Мы пишем так:

var size:int = 0x100;
size &= 0xFF;
// outputs : 0
trace(size.toString(16));


Теперь, когда мы разобрались с основами, познакомимся с классом ByteArray


Класс ByteArray



Самое время использовать все концепции, которые мы обсудили ранее, с помощью класса flash.utils.ByteArray. Тяжело описать всю мощь этого класса в нескольких строчках, поэтому посмотрим на список проектов, где этот класс использовался:

• FC64. Эмулятор Commodore 64, http://codeazur.com.br/stuff/fc64_final/ ;
• Intel8080 : Intel8080 “Space Invaders” CPU Emulation : http://www.bytearray.org/?p=622 ;
• AlivePDF. Библиотека для генерации PDF, http://alivepdf.bytearray.org ;
• FZip. Библиотека для создания ZIP, http://codeazur.com.br/lab/fzip/ ;
• GIF Player. Библиотека для создания проигрывателей GIF, http://www.bytearray.org/?p=95 ;
• GIF Encoder. Библиотека для создания GIF, http://www.bytearray.org/?p=93 ;
• WiiFlash. Библиотека Wii peripherals, http://wiiflash.bytearray.org ;
• Encodeurs d’images. Классы для создания JPEG и PNG, http://code.google.com/p/as3corelib/ ;
• Popforge Audio. Библиотека для генерации звуков в рантайме, http://www.popforge.de/.

Без доступа к байтам эти проекты никогда бы не были реализованы. Сила класса ByteArray в том, что он позволяет управлять данными на низком уровне. Другими словами, мы сможем управлять и хранить данные на уровне байтов, что не было доступно в предыдущих версиях языка ActionScript.

Это позволяет нам, например, интерпретировать или генерировать любой тип файла в ActionScript 3 так скоро, насколько это позволит производительность клиента. Как видно из названия, экземпляр класса ByteArray представляет собой массив байтов (см. рисунок 11).

Рисунок 11. – Байтовый массив


Рисунок 11. – Байтовый массив



У класса ByteArray есть много общего с классом Array. Оба объекта – массивы и таким образом имеют длину, однако в каждой ячейке массива байтов (ByteArray) может храниться значение ограниченное восемью битами, что не позволяет хранить числа превышающие 255.

Заметим, что те же возможности класса ByteArray имеют следующие классы flash.net.Socket и flash.net.URLStream.


Методы записи и чтения



Для создания байтового массива используем подобный код:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();


Для получения значения длины массива используем свойство length:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
// outputs : 0
trace (bytes.length);


Когда массив только создан, его длина равна нулю.

Как мы раньше говорили, порядок байтов может меняться в зависимости от платформы. Свойство endian определяет порядок байтов в экземпляре класса ByteArray.
По умолчанию в ByeArray используется порядок от старшего к младшему (big-endian):

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
// outputs : bigEndian
trace (bytes.endian);


Для изменения порядка записи или чтения байтов мы используем константы класса flash.utils.Endian:

- Endian.BIG_ENDIAN: старший бит в начальной позиции;
- Endian.LITTLE_ENDIAN: младший бит в начальной позиции;

В следующем коде мы проверяем текущий порядок байтов:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
// outputs : true
trace(bytes.endian == Endian.BIG_ENDIAN);


Как видите, порядок от старшего к младшему установлен по умолчанию, но он может быть легко изменен:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
// we will be using little-endian for byte ordering
bytes.endian = Endian.LITTLE_ENDIAN;


Для хранения какого-нибудь значения определенного типа данных в байтовом массиве, оно должно быть преобразовано в группу байтов. Например, если мы хотим сохранить число 5, то для 32-х битного числа мы должны его разбить на 4 байта:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:uint = 5;
// manual writing (big-endian)
bytes[0] = (value & 0xFF000000) >> 24;
bytes[1] = (value & 0x00FF0000) >> 16;
bytes[2] = (value & 0x0000FF00) >> 8;
bytes[3] = value & 0xFF;


Для чтения этого значения из байтового массива пишем следующий код:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:uint = 5;
// manual writing (big-endian)
bytes[0] = (value & 0xFF000000) >> 24;
bytes[1] = (value & 0x00FF0000) >> 16;
bytes[2] = (value & 0x0000FF00) >> 8;
bytes[3] = value & 0xFF;
var finalValue:uint = bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3];
// outputs : 5
trace( finalValue );


Для хранения байтов в порядке от младшего к старшему мы просто изменяем порядок записи и чтения

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:uint = 5;
// manual writing (little-endian)
bytes[3] = (value & 0xFF000000) >> 24;
bytes[2] = (value & 0x00FF0000) >> 16;
bytes[1] = (value & 0x0000FF00) >> 8;
bytes[0] = value & 0xFF;
var finalValue:uint = bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0];
// outputs : 5
trace( finalValue );


Заметим, что точно такого же результата можно добиться с помощью следующего кода:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:uint = 5;
// manual writing (little-endian)
bytes[3] = (value >> 24) & 0xFF;
bytes[2] = (value >> 16) & 0xFF;
bytes[1] = (value >> 8) & 0xFF;
bytes[0] = value & 0xFF;
var finalValue:uint = bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0];
// outputs : 5
trace( finalValue );


Как видите, довольно утомительно вручную записывать числа в байтовый массив. К счастью Flash Player имеет инструменты для автоматической записи и чтения. Так, чтобы записать целое число без знака используется метод writeUnsignedInt класса ByteArray со следующей сигнатурой:

public function writeUnsignedInt(value:int):void


value – целое число без знака. В результате предыдущий код может быть переписан:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:uint = 5;
// we write a 32-bit unsigned integer
bytes.writeUnsignedInt(value);


Теперь, если посмотрим, как изменилась длина массива, увидим, что записаны четыре байта:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:uint = 5;
// we write a 32-bit unsigned integer
bytes.writeUnsignedInt(value);
// outputs : 4
trace( bytes.length );


Для чтения значения просто используем метод readUnsignedInt:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:uint = 5;
// we write a 32-bit unsigned integer
bytes.writeUnsignedInt(value);
// throws a runtime error
var finalValue:uint = bytes.readUnsignedInt();


Если мы запустим предыдущий код, то появиться следующая ошибка:

Error: Error #2030: End of file detected.


Имейте ввиду, что когда мы записываем данные в байтовый массив, внутренний указатель, определенный свойством position, автоматически перемещается в следующую позицию, где будет происходить запись или чтение:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:uint = 5;
// outputs : 0
trace( bytes.position );
// we write a 32-bit unsigned integer
bytes.writeUnsignedInt(value);
// outputs : 4
trace( bytes.position );


Свойство position – просто точка, которая перемещается, если происходит запись или чтение. Это точка, с которой начнутся последующие запись или чтение (см. рисунок 12).

Рисунок 12


Рисунок 12



Нужно запомнить, что перед тем, как читать данные, вы должны сначала сбросить указатель в позицию, откуда вы хотите начать чтение. После того, как вы это сделаете, можно начинать читать:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:uint = 5;
// we write a 32-bit unsigned integer
bytes.writeUnsignedInt(value);
// reset the pointer so that we can read the 4 following bytes
bytes.position = 0;
// reading a 32-bit unsigned integer
var finalValue:uint = bytes.readUnsignedInt();
// outputs : 5
trace(finalValue);


Заметим, что ошибку чтения мы можем обработать (как и другие ошибки) с помощью конструкции try catch:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:uint = 5;
// we write a 32-bit unsigned integer
bytes.writeUnsignedInt(value);
try
{
// throws a runtime error
trace(bytes.readUnsignedInt());
} catch (e:Error)
{
trace ("Error while reading.");
}


В предыдущем коде мы использовали тип uint для хранения результата, возвращаемого методом readUnsignedInt. Возможно, вы слышали, что тип int более предпочтителен, если нужно уменьшить время выполнения математических операций и циклов. В контексте управления двоичными данными, вы должны всегда использовать соответствующий тип, связанный с методами записи и чтения. В результате мы используем тип uint с методами writeUnsignedInt и readUnsignedInt.

Если вы будете использовать тип int, то код выдаст неверный результат. Следующий пример показывает это в действии. Мы храним 32-х битное число без знака равное 3 000 000 000 в байтовом массиве. При сохранении результата, возвращаемого методом readUnsignedInt, мы используем переменную типа int и в конце получаем неправильный результат:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:uint = 3000000000;
// we write a 32-bit unsigned integer
bytes.writeUnsignedInt(value);
bytes.position = 0;
// reading a 32-bit unsigned integer
var finalValue:int = bytes.readUnsignedInt();
// AVM runtime conversion
// outputs : -1294967296
trace( finalValue );


Целое число без знака, возвращаемое методом readUnsignedInt(), не может быть сохранено в переменной типа int, т.к. максимально возможное значение, которое можно хранить в переменной такого типа это 2 147 483 647.

Используя тип uint, мы не получаем ошибки:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:uint = 3000000000;
// we write a 32-bit unsigned integer
bytes.writeUnsignedInt(value);
bytes.position = 0;
// reading a 32-bit unsigned integer
var finalValue:uint = bytes.readUnsignedInt();
// AVM runtime conversion
// outputs : 3000000000
trace(finalValue);


Посмотрим на рисунок 13 и вспомним, что 32-х битное целое число без знака занимает четыре байта памяти, которые представлены в VM типом uint:


Рисунок 13


Рисунок 13



Вы заметили, что на рисунке 13 используется шестнадцатеричное представление чисел?

Это более удобно, чем десятичное или двоичное представление, поэтому далее мы будем числа представлять в шестнадцатеричном виде:

var value:uint = 0xB2D05E00;
// we write a 32-bit unsigned integer
bytes.writeUnsignedInt(value);


Очень просто мы можем сохранить число 3 000 000 000 в порядке от младшего к старшему:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
bytes.endian = Endian.LITTLE_ENDIAN;
var value:uint = 0xB2D05E00;
// we write a 32-bit unsigned integer
bytes.writeUnsignedInt(value);


Байты записались в порядке от младшего к старшему, мы можем это проверить, если прочитаем каждый байт:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
bytes.endian = Endian.LITTLE_ENDIAN;
var value:uint = 0xB2D05E00;
// we write a 32-bit unsigned integer
bytes.writeUnsignedInt(value);
bytes.position = 0;
// outputs : 0
trace(bytes.readUnsignedByte().toString(16));
// outputs : 5E
trace(bytes.readUnsignedByte().toString(16));
//outputs : D0
trace(bytes.readUnsignedByte().toString(16));
//outputs : B2
trace(bytes.readUnsignedByte().toString(16));


Понятно, что при использовании методов чтения класса ByteArray, преобразования выполняются автоматически:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
bytes.endian = Endian.LITTLE_ENDIAN;
var value:uint = 0xB2D05E00;
// we write a 32-bit unsigned integer
bytes.writeUnsignedInt(value);
bytes.position = 0;
// outputs : b2d05e00
trace(bytes.readUnsignedInt().toString(16));


Мы вернемся к понятию порядка байтов, когда будем создавать парсеры и генераторы файлов. Также при эмуляции процессора, необходимо обращать внимание на порядок байтов CPU.

В некоторых ситуациях, производить преобразования быстрее вручную. В следующем коде мы читаем 16-ти битное число из памяти с помощью метода readShort, который выполняет преобразования порядка байтов автоматически.

Это вынуждает нас установить свойство position так, чтобы мы читали соответствующие данные из потока:

protected function readShort(address:int):int
{
// set the pointer to access memory
memory.position = inAddress;
// return the short (16-bit integer)
return memory.readShort();
}


Мы можем оптимизировать предыдущий код, удалив присваивание свойству position и обращаться к элементам массива указывая индекс напрямую и переставляя байты вручную:

protected function readShort(address:int):int
{
// return the short (16-bit integer)
return memory[int(address+1)] << 8 | memory[address];
}


Эта версия компактнее и быстрее. Мы вручную сдвигаем младший байт влево, тем самым возвращаясь к порядку от младшего к старшему.

Теперь запишем число 5 с помощью метода writeUnsignedInt(), помня, что для хранения этого числа достаточно всего 3 битов:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:uint = 5;
// we write a 32-bit unsigned integer
bytes.writeUnsignedInt(value);
bytes.position = 0;
// reading a 32-bit unsigned integer
var finalValue:uint = bytes.readUnsignedInt();
// outputs : 5
trace( finalValue );


В этом случае для хранения числа 5 мы тратим 3 байта впустую (см. рисунок 14):


Рисунок 14


Рисунок 14



Такое значение можно хранить в одном байте. Для его записи мы можем использовать метод writeByte():

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:uint = 5;
// we write a single byte
bytes.writeByte(value);


Этот байт расположен под индексом 0, рисунок 15 показывает значение этого байта в шестнадцатеричном представлении:

Рисунок 15


Рисунок 15



Чтобы прочитать это значение, мы используем метод readUnsignedByte():

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:uint = 5;
// we write a single byte
bytes.writeByte(value);
bytes.position = 0;
// reading a 32-bit unsigned integer
var finalValue:uint = bytes.readUnsignedByte();
// outputs : 5
trace(finalValue);


Поскольку байт не может хранить более 8 битов, то при попытке записать 9-ти битовое значение мы столкнемся с переполнением байта. В результате этого запишутся только 8 младших битов.

В следующем коде мы конвертируем число 260 из десятичного представления в двоичное:

var value:uint = 260;
// outputs : 100000100
trace (value.toString(2));


Как видите, нам нужно 9 бит для записи числа 260, которое выглядит в двоичном представлении следующим образом:

               1   0 0 0 0 0 1 0 0
|_ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _|
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0


Следующий код показывает эту ситуацию:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:uint = 260;
// we try to store the integer (260) into a single byte (8-bits)
bytes.writeByte(value);
bytes.position = 0;
// we read the byte
// outputs : 4
trace (bytes.readUnsignedByte());


Представляем целое беззнаковое число 4 в двоичном виде:

// we read the byte and show its value in binary base
// outputs : 100
trace (bytes.readUnsignedByte().toString(2));


Это и есть наши три младших бита:

               1   0 0 0 0 0 1 0 0
|_ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _|
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0



Как мы видели ранее, байт может хранить значения от 0 до 255, если это значение без знака. Если число со знаком, то байт может хранить значения от -128 до 127. Если мы хотим хранить знаковое целое число -80, то для его чтения нужно использовать соответствующий метод readByte():

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:int = -80;
// we store the integer (-80) into a single byte
bytes.writeByte(value);
bytes.position = 0;
// we read the byte
// outputs : -80
trace(bytes.readByte());


Мы уже работали с 32-х битными значениями в начале этой главы. Если нужно сохранить значение большее, чем 255, мы должны использовать два байта. Представим число 1200 в двоичном виде:

           1 0 0   1 0 1 1 0 0 0 0
|_ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _|
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0


Для хранения этого значения мы используем метод writeShort() со следующей сигнатурой:

public function writeShort(value:int):void


Метод writeShort() позволяет записывать 16-ти битные значения в байтовый массив. В двух байтах можно хранить значения от -32768 до 32767 или от 0 до 65535 если значение беззнаковое.

В следующем коде, мы записываем 16-ти битное значение в байтовый массив:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:int = 1200;
// we try to store the integer (1200) into a word (16-bits)
bytes.writeShort(value);
// number of bytes written
// outputs : 2
trace(bytes.length);


Тестируя код выше, мы видим, что два байта записаны в поток. Чтобы их прочитать, мы используем метод readShort():

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:int = 1200;
// we store the unsigned integer (1200) into a word (16-bits)
bytes.writeShort(value);
bytes.position = 0;
// we read a 16-bit signed integer
// outputs : 1200
trace(bytes.readShort());


Если мы представим 1200 в двоичном виде, то увидим следующее:

           1 0 0   1 0 1 1 0 0 0 0
|_ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _|
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0


Это значение требует для своего хранения два байта. Если мы вызовем метод readUnsignedByte() для чтения этого значения, мы получим значение каждого байта отдельно:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:int = 1200;
// we store the unsigned integer (1200) into a word (16-bits)
bytes.writeShort(1200);
bytes.position = 0;
// we read the first byte
var firstByte:int = bytes.readUnsignedByte();
// we read the second byte
var secondByte:int = bytes.readUnsignedByte();
// outputs : 4
trace(firstByte);
// outputs : 176
trace(secondByte);
// outputs : 1200
trace (firstByte << 8 | secondByte);


Первый байт хранит значение 4:

           1 0 0
|_ _ _ _ _ _ _ _|
7 6 5 4 3 2 1 0


Второй – 176:

 1 0 1 1 0 0 0 0
|_ _ _ _ _ _ _ _|
7 6 5 4 3 2 1 0


Теперь, ActionScript 3.0 использует стандарт IEEE 754 для сохранения чисел с плавающей запятой посредством метода writeFloat():

public function writeFloat(value:Number):void


В следующем коде мы сохраняем 32-х битное число с плавающей запятой в байтовый массив:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:Number = 12.5;
// we write a 32-bit float
bytes.writeFloat(value);
// set the pointer to 0
bytes.position = 0;
// we read the float
var float:Number = bytes.readFloat();
// outputs : 12.5
trace(float);


Число с плавающей запятой требует для своего хранения четыре байта:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var value:Number = 12.5;
// we write a 32-bit float
bytes.writeFloat(value);
// outputs : 4
trace(bytes.length);


Если вы хотите сохранить число с плавающей запятой, которое требует для своего хранения больше чем 32 бита, то используйте метод writeDouble():

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
// we write a 64-bit float
bytes.writeDouble(12.5);
// outputs : 8
trace(bytes.length);
bytes.position = 0;
// outputs : 12.5
trace(bytes.readDouble());


Заметим, что double эквивалентно типу Number и использует 64 бита.


Запись строки в последовательность байтов



Теперь мы посмотрим, как можно записать текст в последовательность байтов. Для этого используется метод writeUTFBytes() со следующей сигнатурой:

public function writeUTFBytes(value:String):void


Каждый символ записывается в байтовый массив как в следующем коде:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var chars:String = "Bonjour Bob";
// outputs : 11
trace(chars.length);
// we write the string into the stream
bytes.writeUTFBytes(chars);
// outputs : 11
trace(bytes.length);


Если мы используем только первые 127 символов ASCII, то для записи одного символа потребуется один байт. Используя метод readByte() мы можем прочитать код каждого символа:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var chars:String = "Hey there!";
// outputs : 10
trace(chars.length);
// we write the string into the stream
bytes.writeUTFBytes(chars);
// outputs : 10
trace(bytes.length);
bytes.position = 0;
// outputs : 72
trace(bytes.readByte());
// outputs : 101
trace(bytes.readByte());


Передавая код символа в метод fromChartCode() класса String, мы получим сам символ:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
var chars:String = "Hey there!";
// outputs : 10
trace(chars.length);
// we write the string into the stream
bytes.writeUTFBytes(chars);
// outputs : 10
trace(bytes.length);
bytes.position = 0;
// outputs : H
trace(String.fromCharCode(bytes.readByte()));
// outputs : e
trace(String.fromCharCode(bytes.readByte()));
// outputs : y
trace(String.fromCharCode(bytes.readByte()));
// outputs :
trace(String.fromCharCode(bytes.readByte()));
// outputs : t
trace(String.fromCharCode(bytes.readByte()));
// outputs : h
trace(String.fromCharCode(bytes.readByte()));
// outputs : e
trace(String.fromCharCode(bytes.readByte()));
// outputs : r
trace(String.fromCharCode(bytes.readByte()));
// outputs : e
trace(String.fromCharCode(bytes.readByte()));
// outputs : !
trace(String.fromCharCode(bytes.readByte()));


Конечно, если используются специальные символы, то понадобиться больше места для их хранения. В следующем коде мы используем символ «ç» из французского алфавита:

var bytes:ByteArray = new ByteArray();
var chars:String = "Bonjour Bob ça va ?";
// outputs : 19
trace(chars.length);
bytes.writeUTFBytes(chars);
// outputs : 20
trace(bytes.length);


Такой символ для своего хранения требует два байта. Есть другой метод для записи строк в байтовый массив, этот метод называется writeUTF():

public function writeUTF(value:String):void


Он почти не отличается от writeUTFBytes(), единственное отличие в том, что строке будет предшествовать 16-битное целое число, хранящее длину строки. В следующем примере записываем символ «b» в байтовый массив с помощью метода writeUTF():

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
// we write a single character
bytes.writeUTF("b");
// outputs : 3
trace(bytes.length);


Если бы мы использовали метод writeUTFBytes(), то длина массива была бы равна единице. А сейчас три – т.к. два байта отведены для хранения числа, которое представляет длину строки. Мы можем прочитать это значение с помощью метода readShort():

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
// we write a single character
bytes.writeUTF("b");
// outputs : 3
trace(bytes.length);
bytes.position = 0;
// outputs : 1
trace(bytes.readShort());


Символ можно прочитать следующим образом:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
// we write a single character
bytes.writeUTF("b");
// outputs : 3
trace(bytes.length);
bytes.position = 0;
// outputs : 1
trace(bytes.readShort());
// we retrieve the char code
var characterCode:uint = bytes.readUnsignedByte();
// outputs : b
trace(String.fromCharCode(characterCode));



Чтение строк из последовательности байтов



Как мы ранее видели, неправильное значение свойства position может привести к следующей ошибке:

Error: Error #2030: End of file detected.


Чтобы проследить количество байтов доступных для чтения, мы будем использовать свойство bytesAvailable. Flash Player автоматически вычитает значение position из длины байтового массива для вычисления количества доступных для чтения байтов:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
// we write the string into the stream
bytes.writeUTFBytes("Bob Groove");
// we set the pointer to 0
bytes.position = 0;
// compute manually the bytes available
// outputs : 10
trace(bytes.length - bytes.position);
// outputs : 10
trace(bytes.bytesAvailable);


Для чтения строки из байтовой последовательности, мы используем метод readUTFBytes() со следующей сигнатурой:

public function readUTFBytes(length:uint):String


В этот метод передается количество байтов, которые необходимо прочитать, каждый символ будет декодирован в строку UTF-8. В следующем коде мы читаем только первые три символа:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
// we write the string into the stream
bytes.writeUTFBytes("Bob Groove");
// we set the pointer to 0
bytes.position = 0;
// we extract the three first characters from the stream
// outputs : Bob
trace(bytes.readUTFBytes(3));


Мы можем использовать свойство bytesAvailable для чтения всей строки:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
// we write the string into the stream
bytes.writeUTFBytes("Bob Groove");
// we set the pointer to 0
bytes.position = 0;
// we extract the entire string from the stream
// outputs : Bob Groove
trace(bytes.readUTFBytes(bytes.bytesAvailable));


Мы можем использовать свойство bytesAvailable и цикл while для извлечения каждого символа в цикле:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
// we write the string into the stream
bytes.writeUTFBytes("Bob Groove");
// we set the pointer to 0
bytes.position = 0;
while (bytes.bytesAvailable > 0)
{
/* outputs :
0 : B
1 : o
2 : b
3 :
4 : G
5 : r
6 : o
7 : o
8 : v
9 : e
*/
trace(String.fromCharCode(bytes.readByte()));
}


Если в строке используется специальный символ, то для чтения используем метод readUTFBytes():

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
// we write the string into the Stream including spécial characters
bytes.writeUTFBytes("Bob ça Groove");
// we set the pointer to 0
bytes.position = 0;
// we extract the entire string from the stream
// outputs : Bob ça Groove
trace(bytes.readUTFBytes(bytes.bytesAvailable));


Понятно, что используя метод readByte() мы не сможем прочитать специальный символ:

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
// we write the string into the Stream including spécial characters
bytes.writeUTFBytes("Bob ça Groove");
// we set the pointer to 0
bytes.position = 0;
while (bytes.bytesAvailable > 0)
{
/*
0 : B
1 : o
2 : b
3 :
4 : ?
5 : ?
6 : a
7 :
8 : G
9 : r
10 : o
11 : o
12 : v
13 : e
*/
trace (bytes.position + " : " + String.fromCharCode(bytes.readByte()))
}


Понятно, что метод readUTFBytes() очень удобен для чтения строк записанных с помощью метода writeUTF(). Теперь, предположим, что мы не знаем длины строки, а методу readUTFBytes() в качестве параметра необходимо количество доступных для чтения байтов. Мы создаем переменную, которая будет хранить длину строки, которую получим с помощью метода readShort():

// we create an array of bytes (ByteArray)
var bytes:ByteArray = new ByteArray();
// we write a string
bytes.writeUTF("Hello there!");
// outputs : 14
trace( bytes.length);
bytes.position = 0;
// we retrieve the length of the String following in the stream
var stringLength:uint = bytes.readShort();
// outputs : Hello there!
trace( bytes.readUTFBytes(stringLength));


На этом у меня все :)

1 комментарий:

  1. хух, осилил)))  интересно было прочитать про байтэррэй, спасибо)

    ОтветитьУдалить