0、前言
对于前端程序员来说,平时很少和二进制数据打交道,所以基本上用不到ArrayBuffer,大家对它很陌生,但是在使用WebGL的时候,ArrayBuffer无处不在。浏览器通过WebGL和显卡进行通信,它们之间会发生大量的、实时的数据交互,对性能的要求特别高,它们之间的数据通信必须是二进制的才能满足性能要求,而不能是传统的文本格式。文本格式传递一个 32 位整数,两端的 JavaScript 脚本与显卡都要进行格式转化,将非常耗时。类型化数组的诞生就是为了能够让开发者通过类型化数组来操作内存,大大增强了JavaScript处理二进制数据的能力。
JavaScript类型化数组将实现拆分为缓冲和视图两部分。一个缓冲(ArrayBuffer)描述的是内存中的一段二进制数据,缓冲没有格式可言,并且不提供机制访问其内容。为了访问在缓存对象中包含的内存,你需要使用视图。视图可以将二进制数据转换为实际有类型的数组。一个缓冲可以提供给多个视图进行读取,不同类型的视图读取的内存长度不同,读取出来的数据格式也不同。缓冲和视图的工作方式如下图所示:
1、缓冲(ArrayBuffer)和视图
ArrayBuffer是一个构造函数,可以分配一段可以存放数据的连续内存区域。
var buffer = new ArrayBuffer(8);
上面代码生成了一段8字节的内存区域,每个字节的值默认都是0。1 字节(Byte) = 8 比特(bit),1比特就是一个二进制位(0 或 1)。上面代码生成的8个字节的内存区域,一共有 8*8=64 比特,每一个二进制位都是0。
为了读写这个buffer,我们需要为它指定视图。视图有两种,一种是TypedArray视图,它一共包括9种类型,还有一种是DataView视图,它可以自定义复合类型。 基础用法如下:
var dataView = new DataView(buffer); dataView.getUint8(0) // 0 var int32View = new Int32Array(buffer); int32View[0] = 1 // 修改底层内存
var uint8View = new Uint8Array(buffer);
uint8View[0] // 1
下面来看一个完整的例子:
// 创建一个16字节长度的缓冲 var buffer = new ArrayBuffer(16); // 创建一个视图,此视图把缓冲内的数据格式化为一个32位(4字节)有符号整数数组 var int32View = new Int32Array(buffer); // 我们可以像普通数组一样访问该数组中的元素 for (var i = 0; i < int32View.length; i++) { int32View[i] = i * 2; } // 运行完之后 int32View 为[0,2,4,6] // 创建另一个视图,此视图把缓冲内的数据格式化为一个16位(2字节)有符号整数数组 var int16View = new Int16Array(buffer); for (var i = 0; i < int16View.length; i++) { console.log(int16View[i]); } // 打印出来的结果依次是0,0,2,0,4,0,6,0
相信图片已经很直观的表达了这段代码的意思。这里应该有人会疑问,为什么2、4、6这三个数字会排在0的前面,这是因为x86的系统都是使用的小端字节序来存储数据的,小端字节序就是在内存中,数据的高位保存在内存的高地址中,数据的低位保存在内存的低地址中。就拿上面这段代码举例,上图中内存大小排列的顺序是从左向右依次变大,int32View[1]对应的4个字节,它填入的值是 10 (2的2进制表示),把0补齐的话就是 00000000 00000000 00000000 00000010(中间的分隔方便观看),计算机会倒过来填充,最终会成为 00000010 00000000 00000000 00000000。与小端字节序对应的就是大端字节序,它就是我们平时读数字的顺序。
2、实际场景
在WebGL中有这么一个需求,我要绘制一个带颜色的三角形,这个三角形有三个顶点,每个点有3个坐标和一个RGBA颜色,现在有了三角形的顶点和颜色数据,需要创建一个缓冲,把三角形的数据按顺序填入,然后传输给WebGL。目前的三角形数据是这样的:
var triangleVertices = [ // (x, y, z) (r, g, b, a) 0.0, 0.5, 0.0, 255, 0, 0, 255, // V0 0.5, -0.5, 0.0, 0, 250, 6, 255, // V1 -0.5, -0.5, 0.0, 0, 0, 255, 255 // V2 ];
目标格式是一个ArrayBuffer,它的格式是这样的:
表示坐标的浮点数是32位的,占4个字节,表示颜色的正整数是8位的,占1个字节,因此我们需要创建两个视图来对这个缓冲进行赋值。
var triangleVertices = [ // (x, y, z) (r, g, b, a) 0.0, 0.5, 0.0, 255, 0, 0, 255, // V0 0.5, -0.5, 0.0, 0, 250, 6, 255, // V1 -0.5, -0.5, 0.0, 0, 0, 255, 255 // V2 ]; var nbrOfVertices = 3; // 顶点数量 var vertexSizeInBytes = 3 * Float32Array.BYTES_PER_ELEMENT + 4 * Uint8Array.BYTES_PER_ELEMENT; // 一个顶点所占的字节数 3*4+4*1 = 16 var buffer = new ArrayBuffer(nbrOfVertices * vertexSizeInBytes); // 3 * 16 = 48 三个顶点一共需要的字节数 var positionView = new Float32Array(buffer); var colorView = new Uint8Array(buffer); var positionOffsetInFloats = 0; var colorOffsetInBytes = 12; var k = 0; // 用三角形数据填充arrayBuffer for (var i = 0; i < nbrOfVertices; i++) { positionView[positionOffsetInFloats] = triangleVertices[k]; // x positionView[1 + positionOffsetInFloats] = triangleVertices[k + 1]; // y positionView[2 + positionOffsetInFloats] = triangleVertices[k + 2]; // z colorView[colorOffsetInBytes] = triangleVertices[k + 3]; // r colorView[1 + colorOffsetInBytes] = triangleVertices[k + 4]; // g colorView[2 + colorOffsetInBytes] = triangleVertices[k + 5]; // b colorView[3 + colorOffsetInBytes] = triangleVertices[k + 6]; // a positionOffsetInFloats += 4; // 4个字节的浮点数循环一次要偏移4位 colorOffsetInBytes += 16; // 1个字节的整数循环一次要偏移16位 k += 7; // 原数组一次处理七个数值(三个坐标四个颜色) }
这段代码运行完,就可以得到我们想要的ArrayBuffer。希望大家可以在浏览器控制台运行一下,然后看看positionView和colorView里面的数据验证一下。细心的小伙伴会发现,如果使用positionView访问颜色数据,或者colorView访问位置数据,得到的数据是“奇怪”的,不知道原因的读者朋友可以去了解一下原码、补码、IEEE浮点数相关的知识。
3、总结
类型化数组的内容还有很多,在这里我只重点介绍了一下缓冲和视图是如何一起合作来管理内存的。
类型化数组的出现最大的作用就是提升了数组的性能,js中Array的内部实现是链表,可以动态增大减少元素,但是元素多的话,性能会比较差,类型化数组管理的是连续内存区域,知道了这块内存的起始位置,可以通过起始位置+N * 偏移量(一次加法一次乘法操作)访问到第N个位置的元素,而Array的话就需要通过链表一个一个的找下去。
类型化数组的使用场景并不多,可以说是为WebGL量身定做的,不过还是希望你能在以后遇到大量数据的场景能够想起来JS的类型化数组这个功能。