JavaScript型別化陣列(二進位制陣列)
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的型別化陣列這個功能。