目錄
- 1. 資源封裝與請求封裝
- 1.1. 請求的封裝 - Request 與其調度器
- 1.2. 資源類 - Resource
- 1.3. 延遲請求與最大請求個數限制
- 1.4. 常用請求方法
- 1.5. 舉例
- 2. 多執行緒技術
- 2.1. 跳轉器
- 2.2. 基本用法
- 2.3. 使用 WebAssembly
- ① 例:解碼 draco 壓縮的幾何資料
- ② 例:處理幾何資料
CesiumJS 對需要網路請求的一切資源都進行了統一的封裝,也就是 Resource 類,
在 XHR 技術橫行的年代,就出現過 ajax 這種神器,但是 Cesium 團隊選擇了自己封裝 XHR,后來 ES6 出現了 Promise API,axios 再次封裝了 XHR,但是 Cesium 團隊對這種底層的改動非常敏感,也是最近一年(2021~2022年)才把 var 改為了 const/let,把 when.js 改為了原生 Promise,把 ""/'' 字串部分改為了 `` 這種反引號字串,因此自封裝的 XHR 就沒有改動,
所以,雖然可能不太常用,我認為還是可以了解了解這套 Resource API 的,
1. 資源封裝與請求封裝
Resource 集成了一些通用的請求方法,以及一些輔助的函式,譬如判斷 blob 的支持、處理 Url(修改QueryString、獲取基地址等)等,不過,真正發起請求,還是得從 Request 和 RequestScheduler 這兩個類說起,
1.1. 請求的封裝 - Request 與其調度器
Request 代表一個具體的請求,RequestScheduler 則是調度器,有人說為什么要整個調度類和調度器類,直接讓 Resource 發起 XHR 請求不就行了嗎?
這與 CesiumJS 的資料調度演算法有關,有的請求并不是馬上隨更新程序就發出的,有的是需要延遲請求的(優先級不同),這時候請求調度器 RequestScheduler 就凸顯了作用,
Request 類一般以 Resource 物件欄位存在:
function Resource(options) {
/* ... */
this.request = defaultValue(options.request, new Request());
/* ... */
}
在需要使用的時候,會把 Resource 物件上的資訊交給 Request 物件:
Resource.prototype.fetch = function (options) {
options = defaultClone(options, {});
options.method = "GET";
return this._makeRequest(options);
};
Resource.prototype._makeRequest = function (options) {
/* ... */
request.url = resource.url;
request.requestFunction = function () {/* ... */};
const promise = RequestScheduler.request(request);
/* ...回傳請求的資料... */
};
1.2. 資源類 - Resource
你可以用很多東西來實體化一個 Resource,你也可以在公開的檔案中看到很多引數是“Resource”型別的,例如幾個很常見的資料類:
/**
* @param {Resource|String|Promise<Resource>|Promise<String>} options.url The url to a tileset JSON file.
*/
function Cesium3DTileset(options) {/* ... */}
/**
* @param {String|Resource} options.url The url to the .gltf or .glb file.
*/
ModelExperimental.fromGltf = function (options) {/* ... */}
/**
* @param {Resource|String|Object} data A url, GeoJSON object, or TopoJSON object to be loaded.
*/
GeoJsonDataSource.load = function (data, options) {/* ... */}
你用這些資訊實體化一個 Resource:
- 資源的網路相對/絕對路徑
Resource實體本身- base64 字串(DataUri) / blob 字串
在各種資料的 API 中也允許你傳入不同的引數,例如 glTF 資料允許你傳遞檔案路徑、glTF JSON 本身甚至是自己請求下來的 gltf/glb 檔案的二進制流,詳見本系列文章的第 6 篇,
Resource 類有很多個發起請求的方法,有實體上的,也有靜態的,在 1.4 小節會列舉,靜態方法會 new 一個 Resource 實體,然后呼叫其對應的實體方法:
Resource.fetchJson = function (options) {
const resource = new Resource(options);
return resource.fetchJson(); // 回傳 Promise<object>
};
在現在 axios 或瀏覽器原生 Fetch API 已經如此通用的環境下,已經很少有需要創建 Resource 物件的需求了,原始碼中一般會使用 Resource.createIfNeeded() 來創建資源物件,在測驗用例中,創建 ktx2 檔案資源的代碼如下:
const resource = Resource.createIfNeeded("./Data/Images/Green4x4.ktx2");
最后說說觸發請求后的呼叫鏈,
以請求 JSON 為例:
Resource.prototype.fetchJson
┕ Resource.prototype.fetch
┕ Resource.prototype._makeRequest
[Module RequestScheduler.js]
┕ RequestScheduler.request
┕ fn startRequest
[Module Request.js]
┕ Request.prototype.requestFunction
[Module Resource.js]
┕ Resource._Implementations.loadWithXhr
_makeRequest 方法會為 Resource 物件的 request 成員注冊請求方法 requestFunction,隨后讓 RequestScheduler 發起請求,一波周轉后,還是會執行這個注冊了的 requestFunction 的,內部會呼叫 Resource._Implementations.loadWithXhr 這個方法,也就是發起 XHR 請求,回傳一個 Promise,
1.3. 延遲請求與最大請求個數限制
按理說,一般是不需要去修改 Request 或 RequestScheduler 上的資訊的,這兩個在 CesiumJS 封裝的請求功能中屬于底層,用 Resource 暴露出來的請求 API 即可,不過,我仍然覺得有兩個地方值得分享,
雖然
RequestScheduler是公開的 API,在 2019 年一個 PR CesiumGS/cesium#8549 中被公開出來了
一個是 RequestScheduler 上的靜態引數 maximumRequests 和 maximumRequestsPerServer,這兩個數值代表的意義是允許開發者設定的最大并發請求數量、每個服務器允許的最大請求數量,默認分別是 50、6,
如果你的服務器允許,那么你可以稍微把這個數值改大一些(譬如并發用戶數不多的時候,私網環境),在請求 3DTiles 瓦片、地球瓦片時能同時多請求一些資源,
另一個是 RequestScheduler 上的延遲請求機制,允許創建 Request 時指定 throttle 引數為 true,在使用 RequestScheduler.request 發出請求時,先暫存起來:
RequestScheduler.request = function (request) {
/* 前面的代碼是非延遲請求的處理 */
const removedRequest = requestHeap.insert(request);
return issueRequest(request);
};
此處 requestHeap 是 CesiumJS 自己制作的一個堆資料結構,
然后在 Scene 的渲染函式執行完畢后,在 postPassesUpdate 函式中呼叫 RequestScheduler.update 將延遲的請求統一再發出:
// Scene.js
function postPassesUpdate(scene) {
/* ... */
RequestScheduler.update();
}
Scene.prototype.render = function (time) {
/* ... */
if (shouldRender) {
/* ... */
tryAndCatchError(this, render);
}
/* ... */
tryAndCatchError(this, postPassesUpdate);
/* ... */
};
這樣就實作了延遲請求,減輕大量請求可能造成的主執行緒壓力 —— 這也就是 RequestScheduler 的職能所在,
想知道哪些資料類具備延遲請求行為?只需在 Source 檔案夾下全代碼搜索 throttle: true 即可,例如 Multiple3DTileContent 在請求內部瓦片內容時,就用了延遲請求;有幾個影像、地形供給器類也用了延遲請求,
1.4. 常用請求方法
Cesium 封裝了 HTTP 常用的請求方法:
- Resource.fetch(這個對應 GET 請求)
- Resource.head
- Resource.patch
- Resource.post
- Resource.delete
- Resource.options
- Resource.put
同時,對常用的檔案/資料格式也做了簡易的封裝,如果你不想用 axios 或 Fetch API,而且能用 async/await 語法,那么直接 await 它們的執行結果也是不錯的,回傳的就是你所需要的資料結果:
- Resource.fetchJson
- Resource.fetchXML
- Resource.fetchImage
- Resource.fetchJsonp
- Resource.fetchText
- Resource.fetchBlob
1.5. 舉例
這里就不再贅述,給出兩個源代碼中的資源創建、請求例子,
一個是 3DTiles 的 tileset.json 入口檔案,用到了 Resource.prototype.fetchJson 方法:
// Cesium3DTileset.js
function Cesium3DTileset(options) {
this._readyPromise = Promise.resolve(options.url)
.then(function (url) {
/* ... */
resource = Resource.createIfNeeded(url);
/* ... */
return Cesium3DTileset.loadJson(resource);
})
/* ... */
}
Cesium3DTileset.loadJson = function (tilesetUrl) {
const resource = Resource.createIfNeeded(tilesetUrl);
return resource.fetchJson();
};
另一個是 ImageryProvider 的靜態函式,請求影像瓦片圖片,用到了 Resource.prototype.loadImage 方法:
ImageryProvider.loadImage = function (imageryProvider, url) {
/* ... */
const resource = Resource.createIfNeeded(url);
if (ktx2Regex.test(resource.url)) {
return loadKTX2(resource);
} else if (
defined(imageryProvider) &&
defined(imageryProvider.tileDiscardPolicy)
) {
return resource.fetchImage({
preferBlob: true,
preferImageBitmap: true,
flipY: true,
});
}
return resource.fetchImage({
preferImageBitmap: true,
flipY: true,
});
};
其余的請讀者自行研究學習,
2. 多執行緒技術
CesiumJS 使用了 WebWorker 多執行緒技術,
什么功能要用到 WebWorker 多執行緒呢?畢竟子執行緒與主執行緒的通信成本、能傳遞的資料型別都是對程式有影響的,WebWorker 技術發布之后,在實踐中發現比較合適的任務是資料編解碼或有阻礙你主執行緒效率的任務(尤其是資料解碼,使用 WASM 輔助更佳),
CesiumJS 有兩類任務需要剝離主執行緒,不影響主執行緒的邏輯判斷:資料解碼、幾何資料的處理,資料解碼主要是 basisu 紋理的檔案解碼、draco 幾何壓碩訓沖資料的解碼;幾何資料的處理主要是少量在 Primitive API 中用到的部分 Geometry 合并操作,
而管理起這些 WebWorker 的管理器是 TaskProcessor 類,
2.1. 跳轉器
在介紹 TaskProcessor 類之前,要先介紹一個 叫做 cesiumWorkerBootstrapper 的東西,你可能打開瀏覽器看報錯、網路抓包時,在源代碼頁面看到過這個東西:

它是 TaskProcessor 創建的一個最基本的子執行緒(子 Worker):
// TaskProcessor.js
function getBootstrapperUrl() {
if (!defined(bootstrapperUrlResult)) {
bootstrapperUrlResult = getWorkerUrl("Workers/cesiumWorkerBootstrapper.js");
}
return bootstrapperUrlResult;
}
function createWorker(processor) {
const worker = new Worker(getBootstrapperUrl());
/* ... */
const bootstrapMessage = {
loaderConfig: {
paths: {
Workers: buildModuleUrl("Workers"),
},
baseUrl: buildModuleUrl.getCesiumBaseUrl().url,
},
workerModule: processor._workerPath,
};
worker.postMessage(bootstrapMessage);
worker.onmessage = function (event) {
completeTask(processor, event.data);
};
return worker;
}
而這個 Workers/cesiumWorkerBootstrapper.js 檔案中的 Worker,直接封裝了一個 RequireJS 庫,使用 require 函式去異步請求了當前 TaskProcessor 需要的真正 Worker,真正的 Worker 的 onmessage 就會替換掉 cesiumWorkerBootstapper 的 onmessage,
RequireJS 是 ESModule 尚未完全定稿前社區提供的一種模塊化方案,即著名的“異步模塊定義”的一種實作,有更高級的封裝庫(如 dojo.js),dojo.js 是 ArcGIS JsAPI 的底層依賴,
簡而言之,這個就是個跳轉器,方便接入 CesiumJS 其它模塊,做個簡單的橋梁,

2.2. 基本用法
在需要使用多執行緒的地方,需要實體化一個 TaskProcessor:
const processor = new TaskProcessor(
"path/to/your-worker.js", // worker 的路徑
4 // 你希望這個 taskProcessor 最多激活多少個 WebWorker 在運行
);
// 當你需要用的時候,以傳遞 ArrayBuffer 的所有權情況為例
const needToDecodeData = https://www.cnblogs.com/onsummer/p/new Float32Array(/* ... */)
processor
.scheduleTask(
needToDecodeData,
[needToDecodeData.buffer]
)
.then(result => {
// result 上就有 taskProcessor 處理后的資料
})
路徑如果相對于 Cesium 運行時的 Worker 目錄,如果你所處的環境能用 async/await 會更直觀:
const result = await processor.scheduleTask(
typedArrayData, [typedArrayData.buffer]
)
TaskProcessor.prototype.scheduleTask 就是觸發 Worker 執行的方法,
2.3. 使用 WebAssembly
TaskProcessor 有一個方法用來初始化 WebAssembly 模塊:
processor.initWebAssemblyModule({
modulePath: "ThirdParty/Workers/basis_transcoder.js",
wasmBinaryFile: "ThirdParty/basis_transcoder.wasm",
}) // 回傳
有一些任務,例如 draco 壓縮資料的解碼或者 basisu 紋理的解碼,需要依賴 wasm 模塊,必須等待 wasm 決議、創建完成才能執行 WebWorker,所以一般 TaskProcessor.prototype.initWebAssemblyModule 方法執行之后,才會呼叫 TaskProcessor.prototype.scheduleTask,見下面 Draco 決議的例子,
實際上,現在要使用 wasm 決議的資料也不過是 basisu 紋理與 draco 壓縮幾何資料而已,
① 例:解碼 draco 壓縮的幾何資料
Draco 壓縮幾何資料是一片以字串 "DRACO" 起頭的二進制資料,Draco 則是由 Google 開源的一套使用 C++ 開發的幾何資料壓縮庫,利用了熵編碼相關的演算法,
glTF 2.0 允許使用 Draco 擴展,3DTiles 的點云格式也允許使用這個擴展,
解碼 Draco 資料是由 DracoLoader.js 模塊匯出的“靜態類”DracoLoader 完成的,DracoLoader 上有幾個 decode 方法供使用,
如果是 glTF 中的 draco 資料,那么是由 ResourceCache.loadDraco 觸發的:
ResourceCache.loadDraco = function (options) {
let dracoLoader = ResourceCache.get(cacheKey);
dracoLoader = new GltfDracoLoader(/* ... */);
ResourceCache.load({
resourceLoader: dracoLoader,
});
/* ... */
};
// =========
GltfDracoLoader.prototype.load = function () {
/* ... */
// 層級較深,多重 Promise,見原始碼,這行不代表真實縮進
const decodePromise = DracoLoader.decodeBufferView(decodeOptions);
/* ... */
};
如果是 3DTiles 的點云格式,則是由 PntsLoader.prototype.load 方法觸發的:
function decodeDraco(loader, context) {
const parsedContent = loader._parsedContent;
const draco = parsedContent.draco;
let decodePromise;
/* ... */
decodePromise = DracoLoader.decodePointCloud(draco, context);
/* ... */
/* ...后續處理... */
}
PntsLoader.prototype.load = function () {
/* ... */
const loader = this;
this._promise = new Promise(function (resolve, reject) {
/* ... */
decodeDraco(loader, frameState.context).then(resolve).catch(reject);
/* ... */
});
};
以常見的 glTF 決議為例,也就是從 glTF 的 bufferView 中獲取的 draco 壓縮資料:
DracoLoader.decodeBufferView = function (options) {
const decoderTaskProcessor = DracoLoader._getDecoderTaskProcessor();
if (!DracoLoader._taskProcessorReady) {
// The task processor is not ready to schedule tasks
return;
}
return decoderTaskProcessor.scheduleTask(options, [options.array.buffer]);
};
DracoLoader._getDecoderTaskProcessor = function () {
if (!defined(DracoLoader._decoderTaskProcessor)) {
const processor = new TaskProcessor(
"decodeDraco",
DracoLoader._maxDecodingConcurrency
);
processor
.initWebAssemblyModule({
modulePath: "ThirdParty/Workers/draco_decoder_nodejs.js",
wasmBinaryFile: "ThirdParty/draco_decoder.wasm",
})
.then(function () {
DracoLoader._taskProcessorReady = true;
});
DracoLoader._decoderTaskProcessor = processor;
}
return DracoLoader._decoderTaskProcessor;
};
總會有那么一幀,DracoLoader 的靜態欄位 _taskProcessorReady 會在 wasm 模塊創建完成后被標記為 true,進而觸發 decoderTaskProcessor.scheduleTask 方法,啟動 draco_decoder_nodejs.js Worker 的解碼任務,
② 例:處理幾何資料
Primitive API 中的幾何體與 Globe/QuadtreePrimitive API 用到的地形網格(由高程瓦片采樣計算成幾何網格)都用到了 TaskProcessor 進行多執行緒處理幾何資料,
在幾個地形資料模塊中,可以看到 TaskProcessor 的使用,例如經典的 QuantizedMeshTerrainData:
// 模塊作用域下
const createMeshTaskName = "createVerticesFromQuantizedTerrainMesh";
const createMeshTaskProcessorNoThrottle = new TaskProcessor(createMeshTaskName);
const createMeshTaskProcessorThrottle = new TaskProcessor(
createMeshTaskName,
TerrainData.maximumAsynchronousTasks
);
QuantizedMeshTerrainData.prototype.createMesh = function (options) {
/* ... */
const createMeshTaskProcessor = throttle
? createMeshTaskProcessorThrottle
: createMeshTaskProcessorNoThrottle;
const verticesPromise = createMeshTaskProcessor.scheduleTask(/* ... */)
/* ...進一步處理多執行緒處理后的資料... */
};
Primitive API 的更新程序也用到了:
Primitive.prototype.update = function (frameState) {
/* ... */
if (this.asynchronous) {
loadAsynchronous(this, frameState);
} else { /* ... */ }
/* ... */
};
let createGeometryTaskProcessors;
const combineGeometryTaskProcessor = new TaskProcessor("combineGeometry");
function loadAsynchronous(primitive, frameState) {
if (primitive._state === PrimitiveState.READY) {
if (!defined(createGeometryTaskProcessors)) {
createGeometryTaskProcessors = new Array(numberOfCreationWorkers);
for (i = 0; i < numberOfCreationWorkers; i++) {
createGeometryTaskProcessors[i] = new TaskProcessor("createGeometry");
}
}
/* ...進一步呼叫 taskProcessor 的 scheduleTask 方法... */
} else if (primitive._state === PrimitiveState.CREATED) {
/* ... */
const promise = combineGeometryTaskProcessor.scheduleTask(/* ... */);
/* ... */
}
};
在 loadAsynchronous 這個函式中,會呼叫 Geometry 所需的 _workerName 創建 TaskProcessor:
function loadAsynchronous(primitive, frameState) {
/* ... */
if (primitive._state === PrimitiveState.READY) {
/* ... */
let subTasks = [];
for (i = 0; i < length; ++i) {
/* ... */
subTasks.push({
moduleName: geometry._workerName,
geometry: geometry,
});
}
/* ...之后就會使用 subTasks 陣列并發啟動 TaskProcessor... */
} /* ... */
}
每一種 Geometry 都有一個自己的私有欄位 _workerName,指向運行時 WebWorker 目錄下的 ${_workerName}.js 檔案,例如:
function PolygonGeometry(options) {
/* ... */
this._workerName = "createPolygonGeometry";
/* ... */
}
這里對多執行緒的介紹僅此一斑,但是差不多也講到了應用的大致方面,希望對讀者有所指引,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/498803.html
標籤:GIS
