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
視圖類型 | 說明 | 字節大小 |
Uint8Array | 8位無符號整數 | 1字節 |
Int8Array | 8位有符號整數 | 1字節 |
Uint8ClampedArray | 8位無符號整數(溢出處理不同) | 1字節 |
Uint16Array | 16位無符號整數 | 2字節 |
Int16Array | 16位有符號整數 | 2字節 |
Uint32Array | 32位無符號整數 | 4字節 |
Int32Array | 32位有符號整數 | 4字節 |
Float32Array | 32位IEEE浮點數 | 4字節 |
Float64Array | 64位IEEE浮點數 | 8字節 |
下面來看一個完整的例子:
// 創建一個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的類型化數組這個功能。