Amin

TypedArray 还是 DataView: 理解字节序

Amin · 2017-02-19翻译 · 580阅读 原文链接

不喜长文看此处

在同一台机器上,访问ArrayBuffer的方式不一样就会得到不同的字节序。长话短说:使用 TypedArrayDataView 两种方式去读取同一个ArrayBuffer得到的结果会有所不同。

ArrayBuffer 主要用来高效快速的访问二进制数据,比如 WebGL , Canvas 2D 或者 Web Audio 所使用的数据。在这些场景下,你通常想以充分利用硬件性能的方式,或者最容易通过网络传输的方式来存储数据。

继续阅读,去发现这到底是为神马。

起底 TypedArrays 和 ArrayBuffer

在 ES6 中,有三位有趣的新成员:

  1. ArrayBuffer,设计用于保存给定数量的二进制数据的数据结构。

  2. TypedArray,操作ArrayBuffer的一种视图,每一项都是相同的大小和类型。

  3. DataView,操作ArrayBuffer的另外一种视图,不同的地方是每一项可以有自己的大小和类型.

如果我们想要处理诸如图像或各种文件之类的东西,那么有一个数据结构可以使用一堆字节来处理二进制数据是有道理的。

我们就不深入探讨关于二进制数据如何工作的细节了,来看一个小例子:

var buffer = new ArrayBuffer(2) // array buffer for two bytes
var bytes = new Uint8Array(buffer) // views the buffer as an array of 8 bit integers

bytes[0] = 65 // ASCII for 'A'
bytes[1] = 66 // ASCII for 'B'

现在我们可以把它转换成 Blob,

通过它创建一个 Data URI ,并将其作为一个新的文本文件打开:

var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri)

在新的浏览器窗口中会显示文本‘AB’。

###哪种方式更好?字节序,第一个部分:

由于是通过 TypedArray 来构造一个较大的数字的,我们得一个接一个地写了两个字节(16位)。我们还可以使用一个16位的数字来写入两个字符 - 用一条指令写两个字节。

MDN中这张热心肠的表格(叫它红领巾吧)能很好的阐释这个想法:

TypedArrays group one or multiple bytes in an ArrayBuffer

你可以看到,在前面的例子中,我们写入了字符'A',然后写入了字符'B',但是我们也可以用Uint16Array来替代,同时将两个字符写进一个16位的数字里面:

var buffer = new ArrayBuffer(2) // array buffer for two bytes
var word = new Uint16Array(buffer) // views the buffer as an array with a single 16 bit integer

var value = (65 << 8) + 66 // we shift the 'A' into the upper 8 bit and add the 'B' as the lower 8 bit.
word[0] = value // write the 16 bit (2 bytes) into the typed array

// Let's create a text file from them:
var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri)

但是,请等一等? 我们看到的是“BA” 而不是像上次一样得到的“AB”! 天哪噜,发生了什么?

让我们仔细看一下写入数组的值:

65 decimal = 01 00 00 01 binary
66 decimal = 01 00 00 10 binary

// what we did when we wrote into the Uint8Array:
01 00 00 01 01 00 00 10
<bytes[0]-> <bytes[1]->

// what we did when we created the 16-bit number:
var value = (01 00 00 01 00 00 00 00) + 01 00 00 10
= 01 00 00 01 01 00 00 10

你可以看到我们写入 Uint8Array 的16位和写入 Uint16Array 的16位是相同的,为什么结果不同?

答案是,一个值的长度大于一个字节,那么它的字节顺序会因系统的字节序而不同。 让我们来验证一下:

var buffer = new ArrayBuffer(2)
// create two typed arrays that provide a view on the same ArrayBuffer
var word = new Uint16Array(buffer) // this one uses 16 bit numbers
var bytes = new Uint8Array(buffer) // this one uses 8 bit numbers

var value = (65 << 8) + 66
word[0] = (65 << 8) + 66
console.log(bytes) // will output [66, 65]
console.log(word[0] === value) // will output true

当查看单个字节时,我们看到'B'的值确实已经写入缓冲区的第一个字节,而不是'A'的值,但是当我们重新读取这16位数时,它并没有发生变化!

这实际上是因为浏览器默认使用小端字节序。

上面那句话是什么意思呢?

让我们假设一个字节可以保存一个数字,因此数字123需要三个字节:123。 小端字节序意味着多字节数字的低位数字首先被存储,因此在存储器中它将被存储为'3','2','1'。(左高右低)

还有大端字节序,其中字节按我们期望的顺序存储,首先从最高位开始,因此在内存中它将被存储为“1”,“2”,“3”。

只要计算机知道数据的存储方式,它可以为我们做转换,并从内存中获得正确的数字。

这没毛病。 来继续秀操作:

var word = new Uint16Array(buffer)
word[0] = value // 如果 isLittleEndian 没有被显式的设置,  isLittleEndian的值会被设置成true或者false.

具体哪个值取决于实现。 然后抉择出对实现最有效的值。

实现必须在每次执行此步骤时使用相同的值,并且必须对GetValueFromBuffer抽象操作中的相应步骤使用相同的值。

好吧,咱们继续开车:我们缺省了isLittleEndian,浏览器会为它确定一个值(在大多数情况下是'true',因为大多数系统都是小端字节序的),后面都会一直用这个值。

这是一个相当合理的行为。正如Dave Herman在博客中指出的,当在规范中选择一种字节序时,它“不是快速模式就是正确模式”。

目前大多数系统都是小端的,所以选择小端字节序这是合情合理的。 当数据采用系统消耗型的格式时,我们能够获得最佳性能,因为我们的数据不需要在可以处理之前进行转换(例如通过GPU、通过WebGL处理)。 除非你明确需要兼容一些稀有硬件,不然的话你可以安全地使用小端字节序,享受更快的速度。

话风一转,如果我们想要通过网络以块的形式传输这些数据,或者写入结构化的二进制文件怎么办?

我们可以很方便的将网络来的数据通过逐字节写入的方式来获取到。 正因如此,我们应该优选大端字节序,因为接下来字节可以顺序的写入。

幸运的是,平台已经知道了我们的痛点!

写入 ArrayBuffers 的另一种方式:DataView

正如我在开始时提到的,需要将不同类型的数据写入 ArrayBuffer 时,DataView 就派上用场了。

想象一下,你想写一个二进制文件,需要一些文件头结构,像这样:

Size in byte Description

2 Identifier “BM” for Bitmap image

4 Size of the image in byte

2 Reserved

2 Reserved

4 Offset (in bytes) between the end of the header and the pixel data

打个小报告(悄悄的): 这是 BMP 的头文件结构 .

使用 DataView ,我们就不用像耍猴一样操作一堆类型化数组了:

var buffer = new ArrayBuffer(14)
var view = new DataView(buffer)

view.setUint8(0, 66)     // Write one byte: 'B'
view.setUint8(1, 67)     // Write one byte: 'M'
view.setUint32(2, 1234)  // Write four byte: 1234 (rest filled with zeroes)
view.setUint16(6, 0)     // Write two bytes: reserved 1
view.setUint16(8, 0)     // Write two bytes: reserved 2
view.setUint32(10, 0)    // Write four bytes: offset

现在ArrayBuffer 包含以下数据:

Byte  |    0   |    1   |    2   |    3   |    4   |    5   | ... |
Type  |   I8   |   I8   |                I32                | ... |    
Data  |    B   |    M   |00000000|00000000|00000100|11010010| ... |

在上面的例子中,我们使用DataView将两个Uint8写入前两个字节,后面跟着一个Uint32占据下面的四个字节,依此类推。

很赞(自嗨脸),现在让我们回到过去。

为了用一个Uint16存储字符串'AB',我们还可以使用DataView,而不是前面提到的Uint16Array:

var buffer = new ArrayBuffer(2) // array buffer for two bytes
var view = new DataView(buffer)

var value = (65 << 8) + 66 // we shift the 'A' into the upper 8 bit and add the 'B' as the lower 8 bit.
view.setUint16(0, value)

// Let's create a text file from them:
var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri)

等等,什么?(一定要蒙逼脸) 我们看到正确的字符串'AB',而不是我们上次写Uint16时得到的'BA'! 难道setUint16默认为大端字节序?

DataView.prototype.setUint16 ( byteOffset, value [ , littleEndian ] )

1. 将 this 赋值给 v.

2. 如果 littleEndian 没有被指定, littleEndian 设置为 false.

3. 返回 SetViewValue(v, byteOffset, littleEndian, “Uint16”, value).

(我太机智了(机智脸).)

可是天杀的! 规范中居然这样写到:缺省的littleEndian值应该被当作falseSetViewValue将继续把这个值传递给SetValueInBuffer,却允许对Uint16Array进行操作时,可以自己把这个值当作true。

这种不匹配导致不同的字节顺序,并且在忽略时可能导致相当多的麻烦。

Khronos 团队在现已过时的 原始规范提案 甚至明确的指出:

类型化数组视图的操作依赖于宿主计算机

DataView 的操作基于指定的端序 (大端字节序或者小端字节序).

这听起来很详尽,但是有一个明显的漏洞:如果操作类型化数组和DataView时,缺省了字节序会怎么样? 答案是:

  • TypedArray 使用系统的端序。

  • DataView 默认为大端序.

结论

那么这里有毛病吗? 没毛病(坚定脸)。

浏览器选择小端字节序,可能是因为在现在大多数系统恰好是运行在CPU和内存级别,这对性能的提升有极大的好处。

那么 TypedArrayDataView 到底有何不同呢?

TypedArray 的目的是提供一种方法来组合二进制数据以便在同一系统上使用 - 因此它选择特定的字节序会更好。

另一方面,DataView 意在用于序列化和反序列化二进制数据以用于传输。 这就是为什么手动选择字节序是有意义的。 大端序的默认值正是因为大端序常用于网络传输(有时称为“网络端序”)。 如果数据被流化,则可以仅通过在下一个存储器位置处添加输入数据来组合数据。

当我们创建的二进制数据离开浏览器时 - 无论是通过网络传输到其他系统还是以文件下载的形式发送给用户,处理二进制数据的最简单的方法是就是使用 DataView

这是很忠恳的建议, 在 this HTML5Rocks article from 2012 也讲到:

通常情况下,当应用程序从服务器读取二进制数据时,需要扫描一次以将其转换为应用程序在内部使用的数据结构。

在此阶段应使用DataView。

将多字节类型数组视图(Int16Array,Uint16Array等)直接与通过 XMLHttpRequest,FileReader 或任何其他输入/输出 API 获取的数据结合使用并不是一个好主意,因为类型化数组视图使用CPU的原生字节序。

总之开了这么久的车,我们学到了:

  • 假定系统是小端序是安全的。

  • TypedArrays 非常适合创建二进制数据,例如传递给 Canvas2D ImageData 或 WebGL。

  • DataView 是一种安全的方式来处理接收到的或者发送到其他系统上的二进制数据。

译者Amin尚未开通打赏功能

相关文章