评论

JS中的二进制数据处理

在现有的计算机中,二进制常常以字节数组的形式存在于程序当中。

前言

  在现有的计算机中,二进制常常以字节数组的形式存在于程序当中。例如在C#里面,就用byte[],标准C里面没有byte类型,但可以通过typedef把byte定义为unsigned char的别名,效果是一样的。JS设计之初似乎就没想过要处理二进制,对于字节的概念可以说是非常非常的模糊。如果要表达字节数组,那么似乎只能用一个普通数组来表示。

  然而随着业务需求的逐渐发展,出现了WebGL这样的技术。所谓WebGL,就是指浏览器与显卡之间的通信接口。为了满足JavaScript与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式。类型化数组(Typed Array)就是在这种背景下诞生的。而类型化数组是建立在ArrayBuffer对象的基础上的。下面介绍一下Arraybuffer。

一、Arraybuffer

1.1 基本概念

ArrayBuffer 对象是 ES6 才纳入正式 ECMAScript 规范,是 JavaScript 操作二进制数据的一个接口。ArrayBuffer 对象是以数组的语法处理二进制数据,也称二进制数组。它不能直接读写,只能通过视图(TypedArray视图和DataView视图)来读写。

 ❝ArrayBuffer 简单说是一片内存,但是你不能直接用它。这就好比你在 C 里面,malloc 一片内存出来,你也会把它转换成 unsigned_int32 或者 int16 这些你需要的实际类型的数组/指针来用。这就是 JS 里的 TypedArray 的作用,那些 Uint32Array 也好,Int16Array 也好,都是给 ArrayBuffer 提供了一个 “View”,MDN 上的原话叫做 “Multiple views on the same data”,对它们进行下标读写,最终都会反应到它所建立在的 ArrayBuffer 之上。❝

1.2 基本操作

「语法」

new ArrayBuffer(length)
  • 参数:length 表示要创建的 ArrayBuffer 的大小,单位为字节;
  • 返回值:ArrayBuffer 对象;
  • 异常:如果 length 大于 Number.MAX_SAFE_INTEGER(>= 2 ** 53)或为负数,则抛出一个 RangeError 异常;

「示例」

const buffer = new ArrayBuffer(32);
buffer.byteLength; // 32
const v = new Int32Array(buffer);
ArrayBuffer.isView(v) // true
const buffer2 = buffer.slice(0, 1);

上面代码表示实例对象 buffer 占用 32 个字节。

它有实例属性 byteLength ,表示当前实例占用的内存字节长度。

它拥有一个静态方法isView(),这个方法可以用来判断是否为TypedArray实例或DataView实例。

它拥有实例方法 slice(),用来复制一部分内存,使用方式同数组的slice方法。

除了slice方法,ArrayBuffer对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。

二、视图

2.1 TypedArray

     TypedArray一共包含九种类型,每一种都是一个构造函数。(DataView视图不支持Uint8ClampedArray,其他都支持)

名称描述长度(字节)Int8Array8位有符号整数1Uint8Array8位无符号整数1Uint8ClampedArray8位无符号整型固定数组(数值在0~255之间)1Int16Array16位有符号整数2Uint16Array16位无符号整数2Int32Array32位有符号整数4Uint32Array32 位无符号整数4Float32Array32 位 IEEE 浮点数4Float64Array64 位 IEEE 浮点数8

每一种视图都有一个BYTES_PER_ELEMENT常数,表示这种数据类型占据的字节数。

Int8Array.BYTES_PER_ELEMENT // 1
Uint8Array.BYTES_PER_ELEMENT // 1
Int16Array.BYTES_PER_ELEMENT // 2
Uint16Array.BYTES_PER_ELEMENT // 2
Int32Array.BYTES_PER_ELEMENT // 4
Uint32Array.BYTES_PER_ELEMENT // 4
Float32Array.BYTES_PER_ELEMENT // 4
Float64Array.BYTES_PER_ELEMENT // 8

  这 9 个构造函数生成的数组,统称为TypedArray视图。它们很像普通数组,都有length属性,普通数组的操作方法和属性,对TypedArray 数组完全适用。 普通数组与 TypedArray 数组的差异主要在以下方面:

TypedArray和Array之间也可以互相转换

const typedArray = new Uint8Array([1, 2, 3, 4]);
const normalArray = Array.apply([], typedArray);

「建立TypedArray视图」

// 创建一个8字节的ArrayBuffer
const a = new ArrayBuffer(8);

// 创建一个指向a的Int32视图,开始于字节0,直到缓冲区的末尾
const a1 = new Int32Array(a);

// 创建一个指向a的Uint8视图,开始于字节4,直到缓冲区的末尾
const a2 = new Uint8Array(a, 4);

// 创建一个指向a的Int16视图,开始于字节4,长度为2
const a3 = new Int16Array(a, 4, 2);

上面代码在一段长度为 8 个字节的内存(a)之上,生成了三个视图:a1、a2和a3。

视图的构造函数可以接受三个参数:

  • 第一个参数(必选):视图对应的底层ArrayBuffer对象;
  • 第二个参数:视图开始的字节序号,默认从 0 开始;
  • 第三个参数:视图包含的数据个数,默认直到本段内存区域结束;

  建立了视图以后,就可以进行各种操作了。这里需要明确的是,视图其实就是普通数组,语法完全没有什么不同,只不过它直接针对内存进行操作,而且每个成员都有确定的数据类型。所以,视图就被叫做“类型化数组”。

「TypedArray视图操作」

const buffer = new ArrayBuffer(8);

const int16View = new Int16Array(buffer);

for (let i = 0; i < int16View.length; i++) {
  int16View[i] = i * 2;
}
console.log(int16View) // [0, 2, 4, 6]

上面代码生成一个8字节的ArrayBuffer对象,然后在它的基础上,建立了一个16位整数的视图。由于每个字节占据8位,那么16位就占据了2个字节(1个字节等于8位),所以一共可以写入4个整数,依次为0,2,4,6。

如果在这段数据上接着建立一个8位整数的视图,则可以读出完全不一样的结果。

const int8View = new Int8Array(buffer);

for (let i = 0; i < int8View.length; i++) {
  int8View[i] = i;
}

console.log(int8View) // [0, 0, 2, 0, 4, 0, 6, 0]

首先整个ArrayBuffer对象会被分成8段。然后,由于x86体系的计算机都采用小端字节序(具体概念理解请自主查询),相对重要的字节排在后面的内存地址,相对不重要字节排在前面的内存地址,所以就得到了上面的结果。还可以看到下面这个例子

const buffer = new ArrayBuffer(4);
const v1 = new Uint8Array(buffer);
v1[0] = 10;
v1[1] = 3;
v1[2] = 11;
v1[3] = 8;

console.log(v1) // [10, 3, 11, 8]

const uInt16View = new Uint16Array(buffer); // [0xa, 0x3, 0xb, 0x8]

console.log(uInt16View) // 计算机采用小端字节序 [0x030a, 0x080b] => [778, 2059]

如果一段数据是大端字节序(大端字节序主要用于数据传输),TypedArray 数组将无法正确解析,因为它只能处理小端字节序!为了解决这个问题,JavaScript 引入DataView对象,可以设定字节序。

2.2 DataView

     DataView 视图是一个可以从二进制 ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。

❝ 字节顺序,又称端序或尾序(英语:Endianness),在计算机科学领域中,指存储器中或在数字通信链路中,组成多字节的字的字节的排列顺序。
字节的排列方式有两个通用规则。例如,一个多位的整数,按照存储地址从低到高排序的字节中,如果该整数的最低有效字节(类似于最低有效位)在最高有效字节的前面,则称小端序;反之则称大端序。在网络应用中,字节序是一个必须被考虑的因素,因为不同机器类型可能采用不同标准的字节序,所以均按照网络标准转化。
例如假设上述变量 x 类型为int,位于地址 0x100 处,它的值为 0x01234567,地址范围为 0x100~0x103字节,其内部排列顺序依赖于机器的类型。大端法从首位开始将是:0x100: 01, 0x101: 23,..。而小端法将是:0x100: 67, 0x101: 45,..。❝

「语法」

new DataView(buffer [, byteOffset [, byteLength]])

相关的参数说明如下:

  • buffer:ArrayBuffer 对象 或 SharedArrayBuffer 对象;
  • byteOffset(可选):此 DataView 对象的第一个字节在 buffer 中的字节偏移。如果未指定,则默认从第一个字节开始;
  • 异常:此 DataView 对象的字节长度。如果未指定,这个视图的长度将匹配 buffer 的长度;

「示例」

const buffer = new ArrayBuffer(16);
const view = new DataView(buffer, 0);

view.setInt8(1, 68);
view.getInt8(1); // 68

  如果一次操作(get或者set)两个或两个以上字节,就必须明确数据的存储方式,到底是小端字节序还是大端字节序。DataView的操作方法默认使用大端字节序解读数据,如果需要使用小端字节序解读,必须在操作方法中指定参数为true(get方法的第二个参数和set方法的第三个参数)。

const buffer = new ArrayBuffer(24);
const dv = new DataView(buffer);
// 1个字节,默认大端字节序
const v1 = dv.getUint8(0);
// 小端字节序
const v1 = dv.getUint16(1, true);
// 大端字节序
const v2 = dv.getUint16(3, false);
// 在第5个字节,以小端字节序写入值为11的32位整数
dv.setInt32(4, 11, true);

  对于直接处理ArrayBuffer对象的业务场景不是特别多,特别是写页面比较多的同学。笔者深刻认识并运用的场景,主要是在处理比较复杂且数据量比较大的点云数据,前端接收到的点云数据已经是原始采集数据转换过的二进制数据,前端需要对二进制数据进行解析,运用的解析方法就是上述提到的各种方法。下面介绍一下业务场景中比较常见到的一种二进制表示类型——Blob。

三、Blob

3.1 基本介绍

  Blob 对象比较常用于文件上传文件读写操作等。在对文件读写的时候,我们更多的时候只是操作File对象,而File继承了所有Blob的属性。所以在我们看来,File对象可以看作一种特殊的Blob对象。

  而Blob 对象与 ArrayBuffer 的区别在于,Blob 对象用于操作二进制文件, ArrayBuffer 用于直接操作内存,所以他们有如下图的关系:

「语法」

const blob = new Blob(array [, options]);

相关的参数说明如下:

  • array:字符串或二进制对象,表示新生成的Blob实例对象的内容;
  • options(可选):比较常用的属性 type,表示数据的 MIME 类型,默认空字符串;

「示例」

const array = ['Hello World!
'];
const blob = new Blob(array, {type : 'text/html'});

「属性和方法」

由上图可以看到,Blob对象拥有size和type两个属性,以及多种自有方法。比较常用的方法slice、arrayBuffer等;slice方法主要用来拷贝原来的数据,返回的也是一个Blob实例,这个方法可以用来做切片上传。arrayBuffer方法返回一个 Promise 对象,包含 blob 中的数据,并在 ArrayBuffer 中以二进制数据的形式呈现。

const blob = new Blob([]);
blob.slice(0, 1);
blob.arrayBuffer().then(buffer => /* 处理 ArrayBuffer 数据的代码……*/);

3.2 运用场景

通过window.URL.createObjectURL方法可以把一个blob转化为一个Blob URL,并且用做文件下载或者图片显示的链接。

Blob URL所实现的下载或者显示等功能,仅仅可以在单个浏览器内部进行。而不能在服务器上进行存储,亦或者说它没有在服务器端存储的意义。

下面是一个Blob的例子,可以看到它很短

blob:d3958f5c-0777-0845-9dcf-2cb28783acaf

和冗长的Base64格式的Data URL相比,Blob URL的长度显然不能够存储足够的信息,这也就意味着它只是类似于一个浏览器内部的“引用“。从这个角度看,Blob URL是一个浏览器自行制定的一个伪协议。

「文件下载」

  

「图片显示」

  

「切片上传」 

 

「本地文件读取」

 

最后一次编辑于  2021-05-10  
点赞 4
收藏
评论

2 个评论

  • 兮
    2022-03-18

    微信里面的worker不支持createImageBitmap


    2022-03-18
    赞同
    回复
  • 寸许月光
    寸许月光
    2021-05-11

    扫盲贴,Mark

    2021-05-11
    赞同
    回复
登录 后发表内容