类型数组

2021/8/31 JavaScript

原文地址 (opens new window)

# 介绍

类型数组对于浏览器来说是一个相对较新的添加,它的出现,是需要有一种有效的方法来处理 WebGL 中的二进制数据。类型数组是一块内存,里面有一个类型化的视图,JavaScript 引擎可以将这一内存直接传递给本地的库,而无需费力地将数据转换为本地化的表达形式。因此,在将数据传递给 WebGL 和其他处理二进制数据的 API 方面,类型数组的性能比 JavaScript 数组好得多。

类型数组视图对一段 ArrayBuffer 的处理,就像单一类型的数组一样。这里是所有常用数值类型的视图,他们有自我介绍的名称,如Float32Array、Float64Array、Int32Array 和 Uint8Array。还有一个特殊的视图 Uint8ClampedArray,它替换了canvas里ImageData中的像素数组类型。

DataView 是第二种视图,它用来处理多种不同类型的数据。 DataView 对象没有类似数组的 API,而是提供了一个 get/set API,用来在任意字节偏移处读取和写入任意数据类型。 DataView 非常适合读取和写入文件头和其他类似结构的数据。

# 使用类型数组的基础知识

# 类型数组视图

要使用类型化数组,需要创建一个 ArrayBuffer 和一个视图。最简单的方法是创建所需大小和类型的类型化数组视图。

// 类型数组视图的工作方式与普通数组非常相似。
var f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];

有多种不同类型的类型化数组视图。它们有相同的 API,因此知道如何使用其中一个,就几乎知道如何使用它们。在下一个示例中,将创建每个当前存在的类型化数组视图。

// 浮点数数组.
var f64 = new Float64Array(8);
var f32 = new Float32Array(16);

// 有符号的整数数组.
var i32 = new Int32Array(16);
var i16 = new Int16Array(32);
var i8 = new Int8Array(64);

// 无符号整数数组.
var u32 = new Uint32Array(16);
var u16 = new Uint16Array(32);
var u8 = new Uint8Array(64);
var pixels = new Uint8ClampedArray(64);

最后一个有点特殊,它将输入值限制在 0 到 255 之间。这对于 Canvas 图像处理算法特别方便,这样就不必手动限制图像处理的计算,来避免 8 位范围溢出。

例如,下面是如何将伽马因子应用于存储在 Uint8Array 中的图像。不是很漂亮:

u8[i] = Math.min(255, Math.max(0, u8[i] * gamma));

使用 Uint8ClampedArray 您可以跳过手动限制:

pixels[i] *= gamma;

创建类型数组视图的另一种方法是先创建一个 ArrayBuffer,然后创建指向它的视图。获得外部数据的API通常以ArrayBuffers为单位,所以这是你获得一个类型数组视图的方式。

var ab = new ArrayBuffer(256); // 256位 ArrayBuffer.
var faFull = new Uint8Array(ab);
var faFirstHalf = new Uint8Array(ab, 0, 128);
var faThirdQuarter = new Uint8Array(ab, 128, 64);
var faRest = new Uint8Array(ab, 192);

对同一个 ArrayBuffer 也可以有多个视图。

var fa = new Float32Array(64);
var ba = new Uint8Array(fa.buffer, 0, Float32Array.BYTES_PER_ELEMENT); // First float of fa.

要把一个类型数组复制到另一个类型数组,最快的方法是使用类型化数组的set方法。对于类似 memcpy 的使用,创建Uint8Arrays 到视图的buffers,并使用set将数据复制过来。

function memcpy(dst, dstOffset, src, srcOffset, length) {
  var dstU8 = new Uint8Array(dst, dstOffset, length);
  var srcU8 = new Uint8Array(src, srcOffset, length);
  dstU8.set(srcU8);
};

# DataView

要使用包含多种不同类型数据的ArrayBuffers,最简单的方法是对缓冲区使用一个DataView。假设我们有一个文件格式,它的头是一个8位无符号int,后面是两个16位int,然后是一个32位浮点数的有效载荷数组。用类型数组视图来读取这个文件是可以做到的,但是有点麻烦。通过DataView,我们可以读取头部,并使用类型数组视图来读取浮点数。

var dv = new DataView(buffer);
var vector_length = dv.getUint8(0);
var width = dv.getUint16(1); // 0+uint8 = 1 位的偏移
var height = dv.getUint16(3); // 0+uint8+uint16 = 3 位的偏移
var vectors = new Float32Array(width*height*vector_length);
for (var i=0, off=5; i<vectors.length; i++, off+=4) {
  vectors[i] = dv.getFloat32(off);
}

在上面的例子中,我读到的所有值都是大-序数。如果缓冲区中的值是小数位的,则可以将可选的 littleEndian 参数传递给 getter:

...
var width = dv.getUint16(1, true);
var height = dv.getUint16(3, true);
...
vectors[i] = dv.getFloat32(off, true);
...

请注意,类型数组视图总是按照本地字节顺序排列。这是为了使它们更快。你应该使用DataView来读取和写入数据,因为字节数会成为一个问题。

DataView也有向缓冲区写值的方法。这些设置器的命名方式与获取器相同,"set "后面是数据类型。

dv.setInt32(0, 25, false); // set big-endian int32 at byte offset 0 to 25
dv.setInt32(4, 25); // set big-endian int32 at byte offset 4 to 25
dv.setFloat32(8, 2.5, true); // set little-endian float32 at byte offset 8 to 2.5

# 字节序的讨论

字节序,或称字节顺序,是指多字节数字在计算机内存中的存储顺序。术语big-endian描述了一种CPU架构,它将最有意义的字节存储在前面;little-endian则将最没有意义的字节存储在前面。在一个给定的CPU架构中使用哪种字节是完全任意的;有很好的理由选择其中之一。事实上,一些CPU可以被配置为同时支持big-endian和little-endian数据。

为什么你需要关注字节序?原因很简单。当从磁盘或网络上读取或写入数据时,必须指定数据的字节序。这可以确保数据被正确地解释,而不考虑正在处理它的CPU的字节数。在我们这个日益网络化的世界里,必须正确支持各种设备,不管是大编码还是小编码,这些设备可能需要处理来自服务器或网络上其他对等物的二进制数据。

DataView接口是专门设计用来从文件和网络中读写数据的。DataView对具有指定字节序的数据进行操作。不管是大的还是小的,都必须在每次访问每个值时指定字节数,以确保在读或写二进制数据时得到一致和正确的结果,不管浏览器运行的CPU的字节数是什么。

通常,当你的应用程序从服务器上读取二进制数据时,你需要对其进行一次扫描,以便将其转换成你的应用程序内部使用的数据结构。在这个阶段应该使用DataView。对于通过XMLHttpRequest、FileReader或其他输入/输出API获取的数据,直接使用多字节类型的数组视图(Int16Array、Uint16Array等)不是一个好主意,因为类型的数组视图使用CPU的本地字节数。稍后会有更多关于这方面的内容。

让我们看几个简单的例子。Windows BMP文件格式在Windows早期曾经是存储图像的标准格式。上面链接的文档清楚地表明,文件中所有的整数值都是以小-序数格式存储的。下面是一个代码片段,它使用本文附带的DataStream.js库解析了BMP头的开头。