Typed Arrays:浏览器中的二进制数据

发布于 2022-05-15 19:47:11 字数 17610 浏览 1115 评论 0

Typed Arrays 是浏览器中相对较新的一个新功能,它的诞生是出于对 WebGL 中处理二进制数据的有效方式的需求。 类型化数组是一块带有类型化视图的内存,就像数组在 C 中的工作方式一样。因为类型化数组是由原始内存支持的,所以 JavaScript 引擎可以将内存直接传递给本地库,而无需费力地转换数据转换为本机表示。 因此,在将数据传递给 WebGL 和其他处理二进制数据的 API 方面,类型化数组的性能比 JavaScript 数组好得多。

类型化的数组视图对 ArrayBuffer 的一段就像单一类型的数组一样。 有所有常用数值类型的视图,具有自描述名称,如 Float32Array、Float64Array、Int32Array 和 Uint8Array。 还有一个特殊的视图已经替换了 Canvas 的 ImageData 中的像素数组类型:Uint8ClampedArray。

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

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

类型化数组视图

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

// Typed array views work pretty much like normal arrays.
var f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];

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

// Floating point arrays.
var f64 = new Float64Array(8);
var f32 = new Float32Array(16);

// Signed integer arrays.
var i32 = new Int32Array(16);
var i16 = new Int16Array(32);
var i8 = new Int8Array(64);

// Unsigned integer arrays.
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-byte 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 到视图的缓冲区并使用 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);
};

数据视图

要使用包含异构类型数据的 ArrayBuffer,最简单的方法是使用 DataView 到缓冲区。 假设我们有一个文件格式,它的标题是一个 8 位无符号整数,后跟两个 16 位整数,然后是一个 32 位浮点数的有效负载数组。 用类型化的数组视图读回来是可行的,但有点痛苦。 使用 DataView,我们可以读取标题并为浮点数组使用类型化数组视图。

var dv = new DataView(buffer);
var vector_length = dv.getUint8(0);
var width = dv.getUint16(1); // 0+uint8 = 1 bytes offset
var height = dv.getUint16(3); // 0+uint8+uint16 = 3 bytes offset
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);
}

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

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

请注意,类型化的数组视图始终按本机字节顺序排列。 这是为了让他们快速。 您应该使用 DataView 来读取和写入字节序将成为问题的数据。

DataView 还具有将值写入缓冲区的方法。 这些 setter 的命名方式与 getter 相同,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 可以配置为同时支持大端和小端数据。

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

DataView 接口专门设计用于在文件和网络中读取和写入数据。 DataView 对具有 指定字节序 。 必须为每个值的每次访问指定字节序,无论大小,以确保在读取或写入二进制数据时获得一致和正确的结果,无论浏览器运行的 CPU 的字节序是什么。

通常,当您的应用程序从服务器读取二进制数据时,您需要对其进行一次扫描,以便将其转换为您的应用程序内部使用的数据结构。 在此阶段应使用 DataView。 将多字节类型化数组视图(Int16Array、Uint16Array 等)直接用于通过 XMLHttpRequest、FileReader 或任何其他输入/输出 API 获取的数据并不是一个好主意,因为类型化数组视图使用 CPU 的本机字节序。 稍后再谈。

让我们看几个简单的例子。 是 Windows BMP Windows 早期存储图像的标准格式。 上面链接的文档清楚地表明文件中的所有整数值都以小端格式存储。 这是一段代码,它使用 DataStream.js 本文随附

function parseBMP(arrayBuffer) {
  var stream = new DataStream(arrayBuffer, 0,
    DataStream.LITTLE_ENDIAN);
  var header = stream.readUint8Array(2);
  var fileSize = stream.readUint32();
  // Skip the next two 16-bit integers
  stream.readUint16();
  stream.readUint16();
  var pixelOffset = stream.readUint32();
  // Now parse the DIB header
  var dibHeaderSize = stream.readUint32();
  var imageWidth = stream.readInt32();
  var imageHeight = stream.readInt32();
  // ...
}

这是另一个示例,这个示例来自 高动态范围渲染演示 WebGL 示例项目 。 此演示下载代表高动态范围纹理的原始小端浮点数据,并需要将其上传到 WebGL。 这是正确解释所有 CPU 架构上的浮点值的代码片段。 假设变量“arrayBuffer”是一个刚刚通过 XMLHttpRequest 从服务器下载的 ArrayBuffer:

var arrayBuffer = ...;
var data = new DataView(arrayBuffer);
var tempArray = new Float32Array(
  data.byteLength / Float32Array.BYTES_PER_ELEMENT);
var len = tempArray.length;
// Incoming data is raw floating point values
// with little-endian byte ordering.
for (var jj = 0; jj < len; ++jj) {
  tempArray[jj] =
    data.getFloat32(jj * Float32Array.BYTES_PER_ELEMENT, true);
}
gl.texImage2D(...other arguments...,
  gl.RGB, gl.FLOAT, tempArray);

经验法则是:从 Web 服务器接收到二进制数据后,使用 DataView 对其进行一次传递。 读取单个数值并将它们存储在一些其他数据结构中,可以是 JavaScript 对象(用于少量结构化数据)或类型化数组视图(用于大数据块)。 这将确保您的代码在各种 CPU 上都能正常工作。 还可以使用 DataView 将数据写入文件或网络,并确保为 littleEndian 参数, set 以生成您正在创建或使用的文件格式。

请记住,通过网络传输的所有数据都隐含地具有格式和字节序(至少对于任何多字节值而言)。 确保清楚地定义和记录您的应用程序通过网络发送的所有数据的格式。

使用类型化数组的浏览器 API

我将简要概述当前使用类型化数组的不同浏览器 API。 当前的作物包括 WebGL、Canvas、Web Audio API、XMLHttpRequests、WebSockets、Web Workers、Media Source API 和 File API。 从 API 列表中,您可以看到 Typed Arrays 非常适合对性能敏感的多媒体工作以及以高效的方式传递数据。

WebGL

Typed Arrays 的第一次使用是在 WebGL 中,它用于传递缓冲区数据和图像数据。 要设置 WebGL 缓冲区对象的内容,请使用带有类型数组的 gl.bufferData() 调用。

var floatArray = new Float32Array([1,2,3,4,5,6,7,8]);
gl.bufferData(gl.ARRAY_BUFFER, floatArray);

类型化数组也用于传递纹理数据。 这是使用类型化数组传入纹理内容的基本示例。

var pixels = new Uint8Array(16*16*4); // 16x16 RGBA image
gl.texImage2D(
  gl.TEXTURE_2D, // target
  0, // mip level
  gl.RGBA, // internal format
  16, 16, // width and height
  0, // border
  gl.RGBA, //format
  gl.UNSIGNED_BYTE, // type
  pixels // texture data
);

您还需要 Typed Arrays 从 WebGL 上下文中读取像素。

var pixels = new Uint8Array(320*240*4); // 320x240 RGBA image
gl.readPixels(0, 0, 320, 240, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

Canvas 2D

最近 Canvas ImageData 对象与 Typed Arrays 规范一起工作。 现在,您可以获得画布元素上像素的类型化数组表示。 这很有帮助,因为现在您还可以创建和编辑画布像素数组,而无需摆弄画布元素。

var imageData = ctx.getImageData(0,0, 200, 100);
var typedArray = imageData.data // data is a Uint8ClampedArray

XMLHttpRequest2

XMLHttpRequest 得到了 Typed Array 的提升,现在您可以接收 Typed Array 响应,而不必将 JavaScript 字符串解析为 Typed Array。 这对于将获取的数据直接传递给多媒体 API 和解析从网络获取的二进制文件来说非常简洁。

您所要做的就是将 XMLHttpRequest 对象的 responseType 设置为 arraybuffer。

xhr.responseType = 'arraybuffer';

回想一下,从网络下载数据时,您必须注意字节顺序问题! 请参阅上面有关字节序的部分。

File APIs

FileReader 可以将文件内容作为 ArrayBuffer 读取。 然后,您可以将类型化的数组视图和 DataView 附加到缓冲区以操作其内容。

reader.readAsArrayBuffer(file);

您也应该在这里记住字节顺序。 有关详细信息,请查看字节顺序部分。

可转移对象

postMessage 中的可传输对象使将二进制数据传递给其他窗口和 Web Worker 的速度大大加快。 当您将对象作为 Transferable 发送给 Worker 时,该对象在发送线程中变得不可访问,而接收 Worker 将获得该对象的所有权。 这允许高度优化的实现,其中不复制发送的数据,只是将类型化数组的所有权转移给接收者。

要将 Transferable 对象与 Web Workers 一起使用,您需要在 worker 上使用 webkitPostMessage 方法。 webkitPostMessage 方法的工作方式与 postMessage 类似,但它需要两个参数而不是一个参数。 添加的第二个参数是您希望传输给工作人员的对象数组。

worker.webkitPostMessage(oneGBTypedArray, [oneGBTypedArray]);

为了从工作人员那里取回对象,工作人员可以以相同的方式将它们传递回主线程。

webkitPostMessage({results: grand, youCanHaveThisBack: oneGBTypedArray}, [oneGBTypedArray]);

Zero copies, woo!

Media Source API

形式获得了一些 Typed Array 的优点 Media Source API 。 您可以使用 webkitSourceAppend 直接将包含视频数据的类型化数组传递给视频元素。 这使得视频元素在现有视频之后附加视频数据。 SourceAppend 非常适合做插页式广告、播放列表、流媒体和其他您可能希望使用单个视频元素播放多个视频的用途。

video.webkitSourceAppend(uint8Array);

Binary WebSockets

您还可以将 Typed Arrays 与 WebSockets 一起使用,以避免必须对所有数据进行字符串化。 非常适合编写高效的协议和最小化网络流量。

socket.binaryType = 'arraybuffer';

这结束了 API 审查。 让我们继续研究用于处理类型化数组的第三方库。

Third-party libraries

jDataView

jDataView 为所有浏览器实现了一个 DataView shim。 DataView 曾经是 WebKit 独有的功能,但现在大多数其他浏览器都支持它。 Mozilla 开发团队正在开发补丁以在 Firefox 上启用 DataView。

Chrome 开发者关系团队的 Eric Bidelman 编写了一个 小型 MP3 ID3 标签阅读器示例 使用 jDataView 这是博客文章中的一个使用示例:

var dv = new jDataView(arraybuffer);

// "TAG" starts at byte -128 from EOF.
// See http://en.wikipedia.org/wiki/ID3
if (dv.getString(3, dv.byteLength - 128) == 'TAG') {
  var title = dv.getString(30, dv.tell());
  var artist = dv.getString(30, dv.tell());
  var album = dv.getString(30, dv.tell());
  var year = dv.getString(4, dv.tell());
} else {
  // no ID3v1 data found.
}

stringencoding

目前在类型化数组中使用字符串有点痛苦,但是有一个 stringencoding 库可以提供帮助。 Stringencoding 实现了提议的 Typed Array 字符串编码规范 ,因此它也是了解即将发生的事情的好方法。

这是字符串编码的基本用法示例:

var uint8array = new TextEncoder(encoding).encode(string);
var string = new TextDecoder(encoding).decode(uint8array);

BitView.js

我为 Typed Arrays 编写了一个小的位操作库,称为 BitView.js。 顾名思义,它的工作原理与 DataView 非常相似,只是它与位一起工作。 使用 BitView,您可以在 ArrayBuffer 中的给定位偏移处获取和设置位的值。 BitView 还具有在任意位偏移处存储和加载 6 位和 12 位整数的方法。

12 位整数非常适合处理屏幕坐标,因为显示器在较长维度上往往少于 4096 像素。 通过使用 12 位整数而不是 32 位整数,可以减少 62% 的大小。 举一个更极端的例子,我正在使用使用 64 位浮点数作为坐标的 Shapefile,但我不需要精度,因为模型只会以屏幕大小显示。 切换到具有 6 位增量的 12 位基本坐标以编码从前一个坐标发生的变化,使文件大小减少到十分之一。 你可以在 这里

以下是使用 BitView.js 的示例:

var bv = new BitView(arrayBuffer);
bv.setBit(4, 1); // Set fourth bit of arrayBuffer to 1.
bv.getBit(17); // Get 17th bit of arrayBuffer.

bv.getBit(50*8 + 3); // Get third bit of 50th byte in arrayBuffer.

bv.setInt6(3, 18); // Write 18 as a 6-bit int to bit position 3 in arrayBuffer.
bv.getInt12(9); // Read a 12-bit int from bit position 9 in arrayBuffer.

DataStream.js

类型化数组最令人兴奋的事情之一是它们如何更容易地在 JavaScript 中处理二进制文件。 您现在可以使用 XMLHttpRequest 获取 ArrayBuffer 并使用 DataView 直接处理它,而不是逐个字符地解析字符串并将字符手动转换为二进制数等。 这使得加载 MP3 文件和读取元数据标签以在您的音频播放器中使用变得容易。 或者加载一个 shapefile 并将其转换为 WebGL 模型。 或者从 JPEG 中读取 EXIF 标签并在幻灯片应用程序中显示它们。

ArrayBuffer XHR 的问题是从缓冲区读取类似结构的数据有点麻烦。 DataView 适合以字节序安全的方式一次读取几个数字,类型化数组视图适合读取元素大小对齐的本机字节序数字数组。 我们觉得缺少的是一种以方便的字节序安全方式读取数组和数据结构的方法。 输入 DataStream.js。

DataStream.js 是一个类型化数组库,它以类似文件的方式从 ArrayBuffers 读取和写入标量、字符串、数组和数据结构。

从 ArrayBuffer 读取浮点数组的示例:

// without DataStream.js
var dv = new DataView(buffer);
var f32 = new Float32Array(buffer.byteLength / 4);
var littleEndian = true;
for (var i = 0; i<f32.length; i++) {
  f32[i] = dv.getFloat32(i*4, littleEndian);
}

// with DataStream.js
var ds = new DataStream(buffer);
ds.endianness = DataStream.LITTLE_ENDIAN;
var f32 = ds.readFloat32Array(ds.byteLength / 4);

DataStream.js 真正有用的地方在于读取更复杂的数据。 假设您有一个读取 JPEG 标记的方法:

// without DataStream.js
var dv = new DataView(buffer);
var objs = [];
for (var i=0; i<buffer.byteLength;) {
  var obj = {};
  obj.tag = dv.getUint16(i);
  i += 2;
  obj.length = dv.getUint16(i);
  i += 2;
  obj.data = new Uint8Array(obj.length - 2);
  for (var j=0; j<obj.data.length; j++,i++) {
    obj.data[j] = dv.getUint8(i);
  }
  objs.push(obj);
}

// with DataStream.js
var ds = new DataStream(buffer);
ds.endianness = ds.BIG_ENDIAN;
var objs = [];
while (!ds.isEof()) {
  var obj = {};
  obj.tag = ds.readUint16();
  obj.length = ds.readUint16();
  obj.data = ds.readUint8Array(obj.length - 2);
  objs.push(obj);
}

或者使用 DataStream.readStruct 方法读取数据结构。 readStruct 方法接受一个包含结构成员类型的结构定义数组。 它有用于处理复杂类型和处理数据数组和嵌套结构的回调函数:

// with DataStream.readStruct
ds.readStruct([
  'objs', ['[]', [ // objs: array of tag,length,data structs
    'tag', 'uint16',
    'length', 'uint16',
    'data', ['[]', 'uint8', function(s,ds){ return s.length - 2; }], // get length with a function
  '*'] // read in as many struct as there are
]);

如您所见,结构定义是 [name, type] 对的平面数组。 嵌套结构是通过为类型设置一个数组来完成的。 数组是通过使用三元素数组定义的,其中第二个元素是数组元素类型,第三个元素是数组长度(作为数字,作为对先前读取的字段的引用或作为回调函数)。 数组定义的第一个元素未使用。

该类型的可能值如下:

Number types

Unsuffixed number types use DataStream endianness.
To explicitly specify endianness, suffix the type with
'le' for little-endian or 'be' for big-endian,
e.g. 'int32be' for big-endian int32.

  'uint8' -- 8-bit unsigned int
  'uint16' -- 16-bit unsigned int
  'uint32' -- 32-bit unsigned int
  'int8' -- 8-bit int
  'int16' -- 16-bit int
  'int32' -- 32-bit int
  'float32' -- 32-bit float
  'float64' -- 64-bit float

String types

  'cstring' -- ASCII string terminated by a zero byte.
  'string:N' -- ASCII string of length N.
  'string,CHARSET:N' -- String of byteLength N encoded with given CHARSET.
  'u16string:N' -- UCS-2 string of length N in DataStream endianness.
  'u16stringle:N' -- UCS-2 string of length N in little-endian.
  'u16stringbe:N' -- UCS-2 string of length N in big-endian.

Complex types

  [name, type, name_2, type_2, ..., name_N, type_N] -- Struct

  function(dataStream, struct) {} -- Callback function to read and return data.

  {get: function(dataStream, struct) {}, set: function(dataStream, struct) {}}
  -- Getter/setter functions to reading and writing data. Handy for using the
     same struct definition for both reading and writing.

  ['', type, length] -- Array of given type and length. The length can be either
                        a number, a string that references a previously-read
                        field, or a callback function(struct, dataStream, type){}.
                        If length is set to ‘*’, elements are read from the
                        DataStream until a read fails.

读取 JPEG 元数据的实时示例 此处查看 。 该演示使用 DataStream.js 读取 JPEG 文件的标签级结构(以及一些 EXIF 解析), 使用 jpg.js 解码和显示 JavaScript 中的 JPEG 图像。

类型化数组的历史

Typed Arrays 开始于 WebGL 的早期实现阶段,当时我们发现将 JavaScript 数组传递给图形驱动程序会导致性能问题。 对于 JavaScript 数组,WebGL 绑定必须分配一个原生数组并通过遍历 JavaScript 数组来填充它,并将数组中的每个 JavaScript 对象转换为所需的原生类型。

为了解决数据转换瓶颈,Mozilla 的 Vladimir Vukicevic 编写了 CanvasFloatArray:一个带有 JavaScript 接口的 C 风格浮点数组。 现在您可以在 JavaScript 中编辑 CanvasFloatArray 并将其直接传递给 WebGL,而无需在绑定中做任何额外的工作。 在进一步的迭代中,CanvasFloatArray 重命名为 WebGLFloatArray,WebGLFloatArray 进一步重命名为 Float32Array 并拆分为支持 ArrayBuffer 和类型化 Float32Array-view 以访问缓冲区。 还为其他整数和浮点大小以及有符号/无符号变体添加了类型。

设计注意事项

从一开始,Typed Arrays 的设计就是为了有效地将二进制数据传递给本地库。 出于这个原因,类型化的数组视图在主机 CPU 的 本机字节序 。 这些决定使 JavaScript 在诸如将顶点数据发送到图形卡等操作期间达到最高性能成为可能。

DataView 专为文件和网络 I/O 设计,其中数据始终具有 指定的字节序 ,并且可能未对齐以获得最佳性能。

内存数据组装(使用类型化数组视图)和 I/O(使用 DataView)之间的设计分离是有意识的。 现代 JavaScript 引擎对类型化数组视图进行了大量优化,并通过它们实现了数值运算的高性能。 这种设计决策使类型化数组视图的当前性能水平成为可能。

References

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

不乱于心

暂无简介

0 文章
0 评论
680 人气
更多

推荐作者

醉城メ夜风

文章 0 评论 0

远昼

文章 0 评论 0

平生欢

文章 0 评论 0

微凉

文章 0 评论 0

Honwey

文章 0 评论 0

qq_ikhFfg

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文