ArrayBuffer
撰写时间:2023-08-06
修订时间:2023-08-06
VBO中存储的数据类型为ArrayBufferView,它是ArrayBuffer的视图。在本章中,我们探索这些类的具体内容。
ArrayBuffer
ArrayBuffer是一个通用的、固定长度的二进制缓冲区。它是由多个字节的数据所组成的数组。
ArrayBuffer有一个byteLength属性(length, counted by bytes,以字节为单位的长度),代表该对象的字节长度,即多少字节。上面的构造函数,创建了一个字节长度为0的实例。
我们可以在构造函数中传入一个字节长度,以创建一个特定长度的实例。其数据被初始化为相应的0值。
视图
不能直接访问ArrayBuffer的中的数据。因此,如果我们试图查看其内容,将显示undefined
:
在Chrome浏览器中运行以下代码:
将在浏览器的Console中输出:
可以看到,对于2个字节的ArrayBuffer,可以由2个Int8Array构成、或2个Uint8Array构成、或1个Int16Array构成。这类似于C语言中使用一个char指针指向一个int的数据类型,以便独立地访问int变量中的每个字节。
我们将上面的Int8Array、Uint8Array、Int16Array这些类型化数组称为ArrayBuffer的视图(view)。也就是说,对于同一个数据,可以根据需求,使用不同的视图将其内在数据转换为不同的数据类型,并依视图的格式进行读写。
注:还有另外一种称为DataView的视图,提供了以特定字长读取或设置缓冲区数据的方法,但由于这种需求较少,故本章不涉及该类的内容。
下面,我们通过类型化数组Uint8Array来访问ArrayBuffer:
显示:
我们取Uint8Array为视图。由于buffer的字长为2,而一个Uint8Array的字长为1,因此buffer的内容依此视图被分为元素为[0, 1]两个元素的数组。byteOffset为0,表示当前指针前向第0个数组元素;length为2,对应于view这个数组共有2个元素。
类型化数组
类型化数组 (typed array) 是一种类似于数组的视图,用于表现其所包含的二进制的数据缓冲区。
类型化数组的本质
类型化数组本质上是数组,因此可以正常访问数组中的数据。
除了直接读写类型化数组的数据之外,对于普通数组的特性、方法,类型化数组也都支持。
视图的含义
上面谈到,我们不能直接访问ArrayBuffer的数据,但如果我们使用类型化数组作为ArrayBuffer的视图,则可以通过类型化数组直接访问ArrayBuffer中的数据。
多视图的问题
但通过类型化数组来访问ArrayBuffer,就意味着我们即可以通过诸如8位的类型化数组来访问,也应允许通过诸如16位的类型化数组来访问。这就是视图的含义:内容相同,但观察、解释同一内容的视图可能不一样。更进一步,不同的视图,导致对相同内容可能有不同的解释。这一部分稍微有些麻烦,我们一步步来探索。
创建了一个2字节的缓冲区buffer。然后,使用Uint8Array的类型化数组作为其视图。这样,我们就可以访问及修改其数据。
接下来,将索引位置为0的元素的数值设置为0
,将索引位置为1的元素的数值设置为2
,然后查看ArrayBuffer的情况。显示:
表示,对于Uint8Array数组来讲,其元素值依序分别为1
, 2
。
而因为Uint8Array是ArrayBuffer的视图,也意味着ArrayBuffer在内存的数据也是依序分别为1
, 2
。
现在,使用Uint16Array作为其视图。
显示:
因为Uint16Array字长为2字节,因此,此视图下,ArrayBuffer只有1个字长为2字节的数组元素,其数值为513
。
现在,问题出现了。第一个问题,Uint16Array字长为2,Uint8Array字长为1,JavaScript引擎在内部是否将Uint8Array的第1个字节通过在前面补0的方式,扩展为2字节的存储空间?如果是这样,因为ArrayBuffer只有2字节,则Uint8Array的第2个元素的值将因为无剩余的空间存储而被丢弃掉。并且,view2的唯一一个元素的值应为1
而不是513
。
因此,JavaScript引擎应当没有扩展第一个元素的值而抛弃第二个元素的值。那为何该值是奇怪的513
?
查看二进制
前面说过,ArrayBuffer是一个通用的二进制数据的缓冲区。要解决该问题,须查看其二进制内存来寻找答案。为此,我编写了一个返回二进制值的函数:
使用方法如下:
第一个参数num为要查看的整数,第二个参数splitNum为每隔多少个数位来插入空格。因此,上面代码的意思为,打印整数5
的二进制,每4位以空格隔开。
显示:
表示,十进制的整数5
的二进制为0000 0101
。
大尾小尾问题
现在,回到上面的问题。先打印view1两个字节的二进制内容:
显示:
view1是以最原始的1个字节为单位来排列的,其顺序也正是ArrayBuffer内存的真实写照。这两个二进制,对应于Uint8Array中的十进制数值1
与2
。
再打印view2两个字节的二进制内容:
显示:
比较这两组二进制数,我们发现,第一个字节与第二个字节交换了位置。而二进制00000010 00000001
正对应于十进制的513
。
我们也发现,JavaScript引擎并没有擅自扩展第一个字节且丢弃第二个字节。实际上,如前所述,view1的内容是ArrayBuffer内存的真实写照,JavaScript引擎根本没有改变其存储顺序。是视图解释的原因,导致了JavaScript引擎将前后两个字节00000001 00000010
解释为00000010 00000001
。
当数据类型有2个或2个以上的字节时,哪个字节表示哪个数位,依赖于系统所采用的小尾(little-endian)表示法还是大尾(big-endian)表示法,是系统据以解释内存的数值的依据。小尾是指在内存存储地址中,从前到后,借十进制的表达,依次为个位、十位、百位...等数位的顺序,即先是小数位,再到大数位;而大尾是指在内存存储地址中,从前到后,借十进制的表达,依次为...百位、十位、个位等数位的顺序,即先是大数位,再到小数位。
例如,对于0x12345678
这个数据,如果采用小尾表示方法,则存储顺序为0x78 0x56 0x34 0x12
;如果采用大尾表示方法,则存储顺序为0x12 0x34 0x56 0x78
。
可见,我的系统对于存储顺序为00000001 00000010
的内存数据,JavaScript引擎依据小尾原则,最终解释为00000010 00000001
的值。
这就是为何我们使用Uint8Array来存储值为1
和2
的两个字节的数值,使用Uint16Array视图后,变成了513
的内在原因。
当然,对于不同数据类型的值的转换,不管使用小尾还是大尾,系统都有一套衡定的算法,我们不必过于操心。本节对于像我们一样普通的程序来讲,只要清楚这一点就行了:换用不同类型的类型化数组视图,与原来视图所表示的值相比,值可能会发生较大的改变;它不是简单的数位扩展及数位补0的问题。
构造函数
类型化数组共有4种构造函数。
指定元素个数的构造函数
在构造函数中传入一个整数,以指定数组的元素个数。
注意细微的区别。ArrayBuffer的构造器参数表示字节数,而类型化数组构造器的参数表示元素个数。这是因为ArrayBuffer没有涉及具体的数据类型,无法表示共有多少个特定类型的元素,因此只能用总共的字节数来表示。而对于每种类型化数组,其属性BYTES_PER_ELEMENT已含有每个元素多少字节的信息,因此,只要指定共有多少个元素,就能计算出总的字节长度。
上面的代码new Int8Array(8)
创建了一个共有8个元素的Int8Array数组,每个元素的字长为1字节,因此其byteLength等于1 * 8 = 8个字节。
代码new Int16Array(8)
创建了一个共有8个元素的Int16Array数组,每个元素的字长为2字节,因此其byteLength等于2 * 8 = 16个字节。
指定数组数据的构造函数
在构造函数中传入一个数组,以作为类型化数组的内容。
我们之前的例子使用最多的就是这种方式。
指定其他类型化数组的构造函数
在构造函数中传入另一个类型化数组。
对于这种方式,两个类型化数组的元素数量及其数据一致,但由于各自的BYTES_PER_ELEMENT不一样,导致最后的byteLength不一样。
指定ArrayBuffer的构造函数
具体详见上面多视图的问题
一节。
各种类型化数组
类型化数组共有11种,详见下表。
类名 | 值域 | 字长 |
---|---|---|
Int8Array | [-128, 127] | 1 |
Uint8Array | [0, 255] | 1 |
Uint8ClampedArray | [0, 255] | 1 |
Int16Array | [-32768, 32767] | 2 |
Uint16Array | [0, 65535] | 2 |
Int32Array | [-2147483648, 2147483647] | 4 |
Uint32Array | [0, 4294967295] | 4 |
Float32Array | [-3.4E38, 3.4E38] | 4 |
Float64Array | [-1.8E308, 1.8E308] | 8 |
BigInt64Array | [-263, 263-1] | 8 |
BigUint64Array | [0, 264-1] | 8 |
从值域来看,WebGL应用中,对于表示顶点位置、顶点颜色等浮点数值,一般使用Float32Array即可;对于顶点索引,一般使用Uint16Array即可。