💴 [ECMASCript6]29. ArrayBuffer
ArrayBuffer对象
ArrayBuffer对象、TypedArray视图和DataView视图是 JavaScript 操作二进制数据的一个接口。这些对象早就存在,属于独立的规格(2011 年 2 月发布),ES6 将它们纳入了 ECMAScript 规格,并且增加了新的方法。它们都是以数组的语法处理二进制数据,所以统称为二进制数组。
这个接口的原始设计目的,与 WebGL 项目有关。所谓 WebGL,就是指浏览器与显卡之间的通信接口,为了满足 JavaScript 与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式。文本格式传递一个 32 位整数,两端的 JavaScript 脚本与显卡都要进行格式转化,将非常耗时。这时要是存在一种机制,可以像 C 语言那样,直接操作字节,将 4 个字节的 32 位整数,以二进制形式原封不动地送入显卡,脚本的性能就会大幅提升。
二进制数组由三类对象组成。
ArrayBuffer对象:代表内存之中的一段二进制数据,可以通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。
(2)TypedArray视图:共包括 9 种类型的视图,比如Uint8Array(无符号 8 位整数)数组视图, Int16Array(16 位整数)数组视图, Float32Array(32 位浮点数)数组视图等等。
(3)DataView视图:可以自定义复合格式的视图,比如第一个字节是 Uint8(无符号 8 位整数)、第二、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还可以自定义字节序。
简单说,ArrayBuffer对象代表原始的二进制数据,TypedArray视图用来读写简单类型的二进制数据,DataView视图用来读写复杂类型的二进制数据。
TypedArray视图支持的数据类型一共有 9 种(DataView视图支持除Uint8C以外的其他 8 种)。
| 数据类型 | 字节长度 | 含义 | 对应的 C 语言类型 |
| Int8 | 1 | 8 位带符号整数 | signed char |
| Uint8 | 1 | 8 位不带符号整数 | unsigned char |
| Uint8C | 1 | 8 位不带符号整数(自动过滤溢出) | unsigned char |
| Int16 | 2 | 16 位带符号整数 | short |
| Uint16 | 2 | 16 位不带符号整数 | unsigned short |
| Int32 | 4 | 32 位带符号整数 | int |
| Uint32 | 4 | 32 位不带符号的整数 | unsigned int |
| Float32 | 4 | 32 位浮点数 | float |
| Float64 | 8 | 64 位浮点数 | double |
DataView视图
如果一段数据包括多种类型(比如服务器传来的 HTTP 数据),这时除了建立ArrayBuffer对象的复合视图以外,还可以通过DataView视图进行操作。
DataView视图提供更多操作选项,而且支持设定字节序。本来,在设计目的上,ArrayBuffer对象的各种TypedArray视图,是用来向网卡、声卡之类的本机设备传送数据,所以使用本机的字节序就可以了;而DataView视图的设计目的,是用来处理网络设备传来的数据,所以大端字节序或小端字节序是可以自行设定的。
DataView实例有以下属性,含义与TypedArray实例的同名方法相同。
DataView实例提供10个方法读取内存。
二进制数组的应用
服务器通过 AJAX 操作只能返回文本数据,即responseType属性默认为text。XMLHttpRequest第二版XHR2允许服务器返回二进制数据,这时分成两种情况。如果明确知道返回的二进制数据类型,可以把返回类型(responseType)设为arraybuffer;如果不知道,就设为blob。
如果知道传回来的是 32 位整数,可以像下面这样处理。
xhr.onreadystatechange = function () {
if (req.readyState === 4 ) {
const arrayResponse = xhr.response;
const dataView = new DataView(arrayResponse);
const ints = new Uint32Array(dataView.byteLength / 4);
xhrDiv.style.backgroundColor = "#00FF00";
xhrDiv.innerText = "Array is " + ints.length + "uints long";
}
}网页Canvas元素输出的二进制像素数据,就是 TypedArray 数组。
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,而且发生运算的时候自动过滤高位溢出。这为图像处理带来了巨大的方便。 3. Websocket
WebSocket可以通过ArrayBuffer,发送或接收二进制数据。
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;
// ···
});如果知道一个文件的二进制数据类型,也可以将这个文件读取为ArrayBuffer对象。
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function () {
const arrayBuffer = reader.result;
// ···
};下面以处理 bmp 文件为例。假定file变量是一个指向 bmp 文件的文件对象,首先读取文件。
const reader = new FileReader();
reader.addEventListener("load", processimage, false);
reader.readAsArrayBuffer(file);
然后,定义处理图像的回调函数:先在二进制数据之上建立一个DataView视图,再建立一个bitmap对象,用于存放处理后的数据,最后将图像展示在Canvas元素之中。
function processimage(e) {
const buffer = e.target.result;
const datav = new DataView(buffer);
const bitmap = {};
// 具体的处理步骤
}
具体处理图像数据时,先处理 bmp 的文件头。具体每个文件头的格式和定义,请参阅有关资料。
bitmap.fileheader = {};
bitmap.fileheader.bfType = datav.getUint16(0, true);
bitmap.fileheader.bfSize = datav.getUint32(2, true);
bitmap.fileheader.bfReserved1 = datav.getUint16(6, true);
bitmap.fileheader.bfReserved2 = datav.getUint16(8, true);
bitmap.fileheader.bfOffBits = datav.getUint32(10, true);
接着处理图像元信息部分。
bitmap.infoheader = {};
bitmap.infoheader.biSize = datav.getUint32(14, true);
bitmap.infoheader.biWidth = datav.getUint32(18, true);
bitmap.infoheader.biHeight = datav.getUint32(22, true);
bitmap.infoheader.biPlanes = datav.getUint16(26, true);
bitmap.infoheader.biBitCount = datav.getUint16(28, true);
bitmap.infoheader.biCompression = datav.getUint32(30, true);
bitmap.infoheader.biSizeImage = datav.getUint32(34, true);
bitmap.infoheader.biXPelsPerMeter = datav.getUint32(38, true);
bitmap.infoheader.biYPelsPerMeter = datav.getUint32(42, true);
bitmap.infoheader.biClrUsed = datav.getUint32(46, true);
bitmap.infoheader.biClrImportant = datav.getUint32(50, true);
最后处理图像本身的像素信息。
const start = bitmap.fileheader.bfOffBits;
bitmap.pixels = new Uint8Array(buffer, start);
至此,图像文件的数据全部处理完成。下一步,可以根据需要,进行图像变形,或者转换格式,或者展示在Canvas网页元素之中。
ShareArrayBuffer
JavaScript 是单线程的,Web worker 引入了多线程:主线程用来与用户互动,Worker 线程用来承担计算任务。每个线程的数据都是隔离的,通过postMessage()通信。
线程之间的数据交换可以是各种格式,不仅仅是字符串,也可以是二进制数据。这种交换采用的是复制机制,即一个进程将需要分享的数据复制一份,通过postMessage方法交给另一个进程。如果数据量比较大,这种通信的效率显然比较低。很容易想到,这时可以留出一块内存区域,由主线程与 Worker 线程共享,两方都可以读写,那么就会大大提高效率,协作起来也会比较简单(不像postMessage那么麻烦)。
ES2017 引入SharedArrayBuffer,允许 Worker 线程与主线程共享同一块内存。SharedArrayBuffer的 API 与ArrayBuffer一模一样,唯一的区别是后者无法共享数据。
Atomics
多线程共享内存,最大的问题就是如何防止两个线程同时修改某个地址,或者说,当一个线程修改共享内存以后,必须有一个机制让其他线程同步。SharedArrayBuffer API 提供Atomics对象,保证所有共享内存的操作都是“原子性”的,并且可以在所有线程内同步。 Atomics对象提供多种方法。
store()方法用来向共享内存写入数据,load()方法用来从共享内存读出数据。比起直接的读写操作,它们的好处是保证了读写操作的原子性。
此外,它们还用来解决一个问题:多个线程使用共享内存的某个位置作为开关(flag),一旦该位置的值变了,就执行特定操作。这时,必须保证该位置的赋值操作,一定是在它前面的所有可能会改写内存的操作结束后执行;而该位置的取值操作,一定是在它后面所有可能会读取该位置的操作开始之前执行。store()方法和load()方法就能做到这一点,编译器不会为了优化,而打乱机器指令的执行顺序。
Worker 线程如果要写入数据,可以使用上面的Atomics.store()方法,也可以使用Atomics.exchange()方法。它们的区别是,Atomics.store()返回写入的值,而Atomics.exchange()返回被替换的值。
使用while循环等待主线程的通知,不是很高效,如果用在主线程,就会造成卡顿,Atomics对象提供了wait()和notify()两个方法用于等待通知。这两个方法相当于锁内存,即在一个线程进行操作时,让其他线程休眠(建立锁),等到操作结束,再唤醒那些休眠的线程(解除锁)。
Atomics.add(sharedArray, index, value)
Atomics.sub(sharedArray, index, value)
Atomics.and(sharedArray, index, value)
Atomics.or(sharedArray, index, value)
Atomics.xor(sharedArray, index, value)
Atomics对象还有以下方法。
Atomics.compareExchange的一个用途是,从 SharedArrayBuffer 读取一个值,然后对该值进行某个操作,操作结束以后,检查一下 SharedArrayBuffer 里面原来那个值是否发生变化(即被其他线程改写过)。如果没有改写过,就将它写回原来的位置,否则读取新的值,再重头进行一次操作。
