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对于前端我们能干啥?
- 处理大量数据:图像、音频、视频等的处理
- 与Web APIs的集成:许多Web APIs(如Canvas、WebGL、Web Audio等)需要操作二进制数据,可以轻松与这些API集成
- 网络通信:与服务器进行数据交换时,使用二进制数据(如字节流)传输数据。
- 内存管理:
ArrayBuffer提供了一种低级别的内存管理机制,开发者可以手动管理内存的分配和释放。这对于一些需要对内存进行精细控制的应用场景非常有用,比如实时图像处理、游戏开发等。 - 高性能计算:对于需要进行大规模数据处理和计算的应用
2 ArrayBuffer的基本概念
2.1 ArrayBuffer的基本结构和工作原理
- 结构:
ArrayBuffer由以下几个部分组成:
- 数据存储区域:连续的、固定大小的内存区域,用于存储二进制数据。
- 字节长度:表示
ArrayBuffer的长度,以字节为单位。 - 视图:
TypedArray视图或DataView视图,用于读写和操作ArrayBuffer中的数据。
- 工作原理:当创建一个
ArrayBuffer时,会分配一块连续的内存空间来存储二进制数据。这块内存空间的大小由创建时指定的字节长度决定。 然后,可以通过TypedArray视图或DataView视图来访问和操作这块内存空间中的数据。ArrayBuffer本身并不提供直接的数据访问方式,而是通过视图来实现数据的读写和操作。
2.2 ArrayBuffer的内部结构,包括数据存储和访问机制
- 数据存储方式:
ArrayBuffer的数据存储方式是连续的、线性的,即所有的二进制数据都存储在一个连续的内存区域中。- 数据存储方式可以是任意的二进制数据,包括整数、浮点数、字节序列等。
- 访问机制:
- 使用
TypedArray视图或DataView视图来访问和操作ArrayBuffer中的数据。 TypedArray提供了一系列的类数组视图,如Uint8Array、Int16Array等,用于直接读写ArrayBuffer中的数据,且数据类型是预定义的。DataView提供了更灵活的数据访问方式,可以指定数据的偏移量和长度,以及数据的字节序,适用于处理复杂的数据结构和格式。
3 ArrayBuffer与TypedArray的关系
3.1 TypedArray,是啥?
如要操作 ArrayBuffer,我们需要使用“视图”对象。
是一种允许直接读写
ArrayBuffer内容的视图。
是一副“眼镜”,透过它来解释存储在
ArrayBuffer中的字节
TypedArray是一组构造函数,一共包含九种类型,每一种都是一个构造函数。
| 名称 | 占用字节 | 描述 |
|---|---|---|
| Int8Array | 1 | 8位有符号整数 |
| Uint8Array | 1 | 8位无符号整数 |
| Uint8ClampedArray | 1 | 8位无符号整型固定数组(数值在0~255之间) |
| Int16Array | 2 | 16位有符号整数 |
| Uint16Array | 2 | 16位无符号整数 |
| Int32Array | 4 | 32 位有符号整数 |
| Uint32Array | 4 | 32 位无符号整数 |
| Float32Array | 4 | 32 位 IEEE 浮点数 |
| Float64Array | 8 | 64 位 IEEE 浮点数 |
示例:
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。
- 原码求补码
-
正整数的补码是其二进制表示,与原码相同。
-
负整数的补码,将其原码除符号外的所有位取反(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图片
- 减少http请求数
- 没有上传文件的条件下,需要插入图片到其他网页。 可以看下Data URL
- css使用方便,background-image: url("data:image/jpg;base64,iVBOR…") ; html使用:src="data:image/jpg;base64,iVBOR…"
- 防止因为相对路径等问题导致图片404的情况
- 为啥又不用
- 无法重复利用和独自缓存,增加内耗。
- 不支持数据压缩,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视图进行高效的数据操作,而不是通过逐个字节的方式进行操作。 在处理大型数据集时,尽量避免频繁的内存分配和释放,以提高性能。
参考文档: