ArrayBuffer(自用留档)

327 阅读13分钟

ArrayBuffer

在现代Web开发中,处理二进制数据变得越来越常见。而ArrayBuffer作为JavaScript中的一个重要特性, 为我们提供了一种高效处理二进制数据的方式。本文将介绍ArrayBuffer的应用场景及性能优化技巧。

1 ArrayBuffer介绍

1.1 ArrayBuffer是什么

ArrayBuffer是内存中的一段连续的固定长度的二进制数据。

let buffer = new ArrayBuffer(length,options);

// 创建一个长度为 16 的 buffer 它会分配一个 16 字节(byte)的连续内存空间,并用 0 进行预填充。
const buffer1 = new ArrayBuffer(16);

  • length: 要创建的数组缓冲区的大小(以字节为单位)

  • options: 一个对象,可以包含以下属性

maxByteLength: 数组缓冲区可以调整到的最大大小,以字节为单位。

注意ArrayBuffer不是某种东西的数组。它和数组Array没有任何共同之处。

  • 它的长度是固定的,无法增加或减少它的长度。
  • 它正好占用了内存中的那么多空间。
  • 要访问单个字节,需要另一个“视图”对象,而不是buffer[index]

ArrayBuffer 具体存储什么?不清楚,只是一个原始的字节序列。

1.2 ArrayBuffer对于前端我们能干啥?

  1. 处理大量数据:图像、音频、视频等的处理
  2. 与Web APIs的集成:许多Web APIs(如Canvas、WebGL、Web Audio等)需要操作二进制数据,可以轻松与这些API集成
  3. 网络通信:与服务器进行数据交换时,使用二进制数据(如字节流)传输数据。
  4. 内存管理:ArrayBuffer提供了一种低级别的内存管理机制,开发者可以手动管理内存的分配和释放。这对于一些需要对内存进行精细控制的应用场景非常有用,比如实时图像处理、游戏开发等。
  5. 高性能计算:对于需要进行大规模数据处理和计算的应用

2 ArrayBuffer的基本概念

2.1 ArrayBuffer的基本结构和工作原理

  1. 结构:ArrayBuffer由以下几个部分组成:
  • 数据存储区域:连续的、固定大小的内存区域,用于存储二进制数据。
  • 字节长度:表示ArrayBuffer的长度,以字节为单位。
  • 视图:TypedArray视图或DataView视图,用于读写和操作ArrayBuffer中的数据。
  1. 工作原理:当创建一个ArrayBuffer时,会分配一块连续的内存空间来存储二进制数据。这块内存空间的大小由创建时指定的字节长度决定。 然后,可以通过TypedArray视图或DataView视图来访问和操作这块内存空间中的数据。ArrayBuffer本身并不提供直接的数据访问方式,而是通过视图来实现数据的读写和操作。

2.2 ArrayBuffer的内部结构,包括数据存储和访问机制

  1. 数据存储方式:
  • ArrayBuffer的数据存储方式是连续的、线性的,即所有的二进制数据都存储在一个连续的内存区域中。
  • 数据存储方式可以是任意的二进制数据,包括整数、浮点数、字节序列等。
  1. 访问机制:
  • 使用TypedArray视图或DataView视图来访问和操作ArrayBuffer中的数据。
  • TypedArray提供了一系列的类数组视图,如Uint8ArrayInt16Array等,用于直接读写ArrayBuffer中的数据,且数据类型是预定义的。
  • DataView提供了更灵活的数据访问方式,可以指定数据的偏移量和长度,以及数据的字节序,适用于处理复杂的数据结构和格式。

3 ArrayBufferTypedArray的关系

3.1 TypedArray,是啥?

如要操作 ArrayBuffer,我们需要使用“视图”对象。

是一种允许直接读写ArrayBuffer内容的视图。

是一副“眼镜”,透过它来解释存储在 ArrayBuffer 中的字节

TypedArray是一组构造函数,一共包含九种类型,每一种都是一个构造函数。

名称占用字节描述
Int8Array18位有符号整数
Uint8Array18位无符号整数
Uint8ClampedArray18位无符号整型固定数组(数值在0~255之间)
Int16Array216位有符号整数
Uint16Array216位无符号整数
Int32Array432 位有符号整数
Uint32Array432 位无符号整数
Float32Array432 位 IEEE 浮点数
Float64Array864 位 IEEE 浮点数

二进制视图.png

示例:

let buffer = new ArrayBuffer(16);
let view = new Uint8Array(buffer);
view[0] = 255;
console.log(view[0]); // 输出 255

4 ArrayBuffer的优势

4.1 ArrayBuffer相对于传统的JavaScript数组的优势

传统的JavaScript数组是动态的,可以自动调整大小,并且可以存储各种类型的数据。然而,对于处理大量二进制数据或者需要更底层控制的情况,ArrayBuffer提供了更好的解决方案,主要体现在以下几个方面:

  • 内存管理: ArrayBuffer提供了一种在内存中分配固定大小的缓冲区的方式,而不像传统数组那样动态调整大小。这意味着在处理大量数据时,可以更有效地管理内存,减少内存分配和释放的开销。

  • 数据类型: ArrayBuffer不是一个通用的数组,而是一种用于存储二进制数据的固定大小的缓冲区。这使得它在处理二进制数据时更加高效,并且可以直接操作底层的二进制数据。

  • 底层操作: ArrayBuffer允许直接对二进制数据进行底层操作,而不需要像传统数组那样通过迭代和复制来处理数据。这使得在处理大量数据时,可以更快地进行操作,提高了性能。


// 创建一个包含10个32位整数的ArrayBuffer
let buffer = new ArrayBuffer(10 * 4); // 4 bytes per int32

// 创建一个指向ArrayBuffer的视图,以便操作其中的数据
let view = new Int32Array(buffer);

// 将数据写入ArrayBuffer
view[0] = 42;
view[1] = 100;

// 从ArrayBuffer中读取数据
console.log(view[0]); // 输出: 42
console.log(view[1]); // 输出: 100

无需像传统数组那样进行迭代和复制

想要知道视图数据类型占据的字节数,Int32Array.BYTES_PER_ELEMENT

4.2 ArrayBuffer在处理大量二进制数据时的高效性和性能优势

ArrayBuffer在处理大量二进制数据时具有高效性和性能优势,主要体现在以下几个方面:

  • 直接内存访问: ArrayBuffer允许直接访问底层的二进制数据,而不需要像传统数组那样进行复制。这减少了在操作大量数据时的内存开销,并提高了访问速度。

  • 并行处理: 由于ArrayBuffer允许直接对二进制数据进行操作,并且不需要锁定整个数组,因此可以更容易地实现并行处理。这使得在多线程或Web Worker中处理大量数据时,可以更好地利用系统资源,提高性能。

  • 底层优化: ArrayBuffer的底层实现通常经过了优化,以提高在处理大量数据时的性能。例如,一些JavaScript引擎可能会针对ArrayBuffer提供特定的优化,以实现更快的数据访问和操作。


// 创建一个包含100万个32位整数的ArrayBuffer
let buffer = new ArrayBuffer(1000000 * 4); // 4 bytes per int32

// 创建一个指向ArrayBuffer的视图,以便操作其中的数据
let view = new Int32Array(buffer);

// 并行处理数据
for (let i = 0; i < view.length; i++) {
    view[i] = i;
}

// 计算数组中所有元素的总和
let sum = 0;
for (let i = 0; i < view.length; i++) {
    sum += view[i];
}

console.log(sum); // 输出: 499999500000

创建了一个包含100万个32位整数的ArrayBuffer,并通过Int32Array视图对其进行操作。然后,我们使用并行处理的方式向数组中填充数据,并计算所有元素的总和。由于ArrayBuffer的优势,我们可以高效地处理这么大规模的数据,而不会出现性能问题。

5 DataView视图

DataView视图提供了一种灵活的方式来读取和写入任意类型的二进制数据,而不受底层数据存储格式的限制。 与TypedArray视图不同,DataView视图可以指定字节序,因此在处理跨平台数据时更加灵活和可靠。

示例:

let buffer = new ArrayBuffer(16);
let view = new DataView(buffer);
view.setInt16(0, 42);
console.log(view.getInt16(0)); // 输出 42

// `get`方法的参数都是一个字节序号(不能是负数,否则会报错),表示从那个字节开始读取。
// 默认情况下,`DataView`的`get`方法使用大端字节序解读数据,如果需要使用小端字节序解读,必须在`get`方法的第二个参数指定true。

// 小端字节序
const v1 = dv.getUint16(1, true);

// 大端字节序
const v2 = dv.getUint16(3, false);

// 大端字节序
const v3 = dv.getUint16(3);

大端字节序和小端字节序,x86体系的计算机都使用小端字节序,123456中12比较重要,所以排在后面,存储顺序是563412。大端则相反

简单说,ArrayBuffer对象代表原始的二进制数据, TypedArray视图用来读写简单类型的二进制数据, DataView视图用来读写复杂类型的二进制数据。

补充:

  • 溢出

不同的视图类型,所能容纳的数值范围是确定的。超出这个范围就会溢出。

TypeArray 数组的溢出规则,简单来说,就是抛弃溢出的位,然后按照视图类型进行解释。

const uint8 = new Uint8Array(1);

uint8[0] = 256;
uint8[0] // 0

uint8[0] = -1;
uint8[0] // 255

uint8是一个 8 位视图,而 256 的二进制形式是一个 9 位的值100000000,这时就会发生溢出。根据规则,只会保留后 8 位,即00000000。uint8视图的解释规则是无符号的 8 位整数,所以00000000就是0。

  • 原码求补码
  1. 正整数的补码是其二进制表示,与原码相同。

  2. 负整数的补码,将其原码除符号外的所有位取反(0变1,1变0,符号位为1不变)后加1

eg:

负数在计算机内部采用“2 的补码”表示,也就是说,将对应的正数值进行否运算,然后加1。比如,-1对应的正值是1,进行否运算以后,得到11111110,再加上1就是补码形式11111111。uint8按照无符号的 8 位整数解释11111111,返回结果就是255。

-5对应正数5(10000101)→所有位取反(11111010)→加1(11111011)

规则如下:

  • 正向溢出(overflow):当输入值大于当前数据类型的最大值,结果等于当前数据类型的最小值加上余值,再减去 1。
  • 负向溢出(underflow):当输入值小于当前数据类型的最小值,结果等于当前数据类型的最大值减去余值的绝对值,再加上 1。

Uint8ClampedArray视图的溢出规则,与上面的规则不同。 它规定,凡是发生正向溢出,该值一律等于当前数据类型的最大值,即 255;如果发生负向溢出,该值一律等于当前数据类型的最小值,即 0。

const uint8c = new Uint8ClampedArray(1);

uint8c[0] = 256;
uint8c[0] // 255

uint8c[0] = -1;
uint8c[0] // 0

6 ArrayBuffer的应用场景

6.1 ArrayBuffer的应用场景

6.1.1 AJAX

responseType属性默认为text。XMLHttpRequest第二版XHR2允许服务器返回二进制数据,这时分成两种情况。如果明确知道返回的二进制数据类型,可以把返回类型(responseType)设为arraybuffer;如果不知道,就设为blob。

let xhr = new XMLHttpRequest();
xhr.open('GET', someUrl);
xhr.responseType = 'arraybuffer';

xhr.onload = function () {
  let arrayBuffer = xhr.response;
  // ···
};

xhr.send();

6.1.2 图像处理

将图像数据存储为二进制数据,并通过TypedArray视图进行像素级操作和处理。

在处理文件时,经常会遇到二进制数据。这里仅以典型的图像处理展开说说。

先看看base64格式的图片数据是什么样式的:图片在线转base64

  • 为啥要使用base64图片
  1. 减少http请求数
  1. 没有上传文件的条件下,需要插入图片到其他网页。 可以看下Data URL
  2. css使用方便,background-image: url("data:image/jpg;base64,iVBOR…") ; html使用:src="data:image/jpg;base64,iVBOR…"
  3. 防止因为相对路径等问题导致图片404的情况
  • 为啥又不用
  1. 无法重复利用和独自缓存,增加内耗。
  2. 不支持数据压缩,base64编码会增加1/3大小,而urlencode后数据量会增加很多。

一般base64是用于小图片上。

在项目中的使用,我们一般把base64转成blob,然后在上传。

项目代码使用示例

/**
 *
 *上传 base64 图片
 *
 * file 无需处理前缀,后端需要参数后缀名,统一在这里提取扩展名
 * moduleName 模块名称
 * type 上传类型 type=image-upload/file-upload/video/
 * @export
 * @param {IDoUploadBase64Props} { file, moduleName }
 * @returns Promise
 */
export default function uploadBase64(options: IDoUploadBase64Props): Promise<IUploadResult> {
  const { file, moduleName, timeout = DEFAULT_TIMEOUT,type='image-upload' } = options;
  let picBase64 = file;

  /**
   *
    base64Data 在将 base64 转换为 Blob 对象后,原始的文件名和扩展名信息会丢失。从file中提取扩展名
   */
  function getExtensionFromBase64(base64Data: string) {
    const matches = base64Data.match(/^data:(.*);base64,/);
    if (matches && matches.length > 1) {
      const mimeType = matches[1];
      const extension = mimeType.split('/')[1];
      return extension;
    }
    return null;
  }

  // 获取文件扩展名
  const extension = getExtensionFromBase64(file);

  // 把头部的data:image/png;base64,去掉。(注意:base64后面的逗号也去掉)
  if (picBase64.includes(';base64,')) {
    // eslint-disable-next-line prefer-destructuring
    picBase64 = picBase64.split(';base64,')[1]
  }

  return new Promise((resolve, reject) => {
    function base64ToBlob(base64: string): Blob {
      const byteCharacters = atob(base64);
      const byteArrays = [];

      for (let offset = 0; offset < byteCharacters.length; offset += 1024) {
        const slice = byteCharacters.slice(offset, offset + 1024);

        const byteNumbers = new Array(slice.length);
        for (let i = 0; i < slice.length; i++) {
          byteNumbers[i] = slice.charCodeAt(i);
        }

        const byteArray = new Uint8Array(byteNumbers);
        byteArrays.push(byteArray);
      }

      return new Blob(byteArrays, { type: 'application/octet-stream' });
    }

    const fileBlob = base64ToBlob(picBase64);

    const formData = new FormData();
    formData.append('file', fileBlob, `${moduleName}.${extension}`);

    formData.append('prefix', `${moduleName}`);
    formData.append('type', `${type}`);

      request.postForm(`file/upload`, formData,{ timeout })
        .then((res:any) => {
          // 处理后端返回的数据
          const { url, key } = res.data;
          const copyFile: any = file;
          resolve({ url, key, name: copyFile.name });
        })
        .catch((error) => {
          // 处理错误
          reject({
            ...error,
          });
        });

  });

6.1.3 网络传输

通过WebSocket或Fetch API发送和接收二进制数据。


let socket = new WebSocket('ws://127.0.0.1:8081');
socket.binaryType = 'arraybuffer';

// Wait until socket is open
socket.addEventListener('open', function (event) {
  // Send binary data
  const typedArray = new Uint8Array(4);
  socket.send(typedArray.buffer);
});

// Receive binary data
socket.addEventListener('message', function (event) {
  const arrayBuffer = event.data;
  // ···
});

6.1.4 canvas

网页canvas元素输出的二进制像素数据,就是TypeArray数组。

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const uint8ClampedArray = imageData.data;

需要注意的是,上面代码的uint8ClampedArray虽然是一个 TypedArray 数组, 但是它的视图类型是一种针对Canvas元素的专有类型Uint8ClampedArray。 这个视图类型的特点,就是专门针对颜色,把每个字节解读为无符号的 8 位整数,即只能取值 0 ~ 255,而且发生运算的时候自动过滤高位溢出。 这为图像处理带来了巨大的方便。

等等……

7 ArrayBuffer的性能优化

确保及时释放ArrayBuffer对象的内存,避免内存泄漏。 使用TypedArray视图进行高效的数据操作,而不是通过逐个字节的方式进行操作。 在处理大型数据集时,尽量避免频繁的内存分配和释放,以提高性能。

参考文档: