微信小游戏背后的技术优化
作者:chrongzhang,騰訊 WXG 客戶端開發(fā)工程師
這是一篇介紹微信小游戲客戶端底層,如果進行優(yōu)化,可以讓所有小游戲獲得更好性能的文章。不是你想像的怎么優(yōu)化某個小游戲的文章。來都來了,就了解一下吧:)
小游戲主要分為渲染和邏輯兩部分。渲染優(yōu)化能讓渲染相關(guān)的指令(WebGL/GFX)得到更高效的執(zhí)行,邏輯優(yōu)化是讓除渲染之外的代碼也能更高效的執(zhí)行,本篇主要講述邏輯相關(guān)的優(yōu)化。
基礎(chǔ)功能優(yōu)化
V8
微信小游戲是在 2017 年 12 月 28 日上線的,當(dāng)時微信安卓客戶端使用的 V8 版本還是 5.5。而 Google 在 V8 上的迭代速度是很快的,其中一個大的版本變更是從 5.9 版本開始,編譯器由原來的 FullCodeGenerator + Crankshaft 變更成更加高效的 Ignition + TurboFan。
markV8 引擎之所以性能高,在于其出色的 JIT 執(zhí)行效率。JIT 依賴了一個可以在運行時優(yōu)化代碼的動態(tài)編譯器。V8 早期的 JIT 編譯器是 FullCodegen,后來是 Crankshaft,然后是一直沿用至今的 Turbofan。
升級 V8,可以獲得更高的執(zhí)行性能(TurboFan)、更快的啟動速度(Snapshot + Code Caching)、更低的內(nèi)存占用(64 位壓縮指針)。小游戲上線至今,客戶端使用的 V8 也一直在升級當(dāng)中,從最初的 5.5,升級到 6.6,然后是 7.6,直到目前的 8.0。
JSBinding
微信小游戲?qū)﹂_發(fā)者暴露的是 JS 的接口,開發(fā)者調(diào)用某些 JS 函數(shù)時,最終會調(diào)用到客戶端底層的原生能力。而從 JS 到客戶端底層之間的橋接能力,就是所謂的 JS 綁定。JS 綁定又分為兩種:裸綁定和非裸綁定。裸綁定是通過 V8/JavaScriptCore 提供的原生接口,將某個 JS 函數(shù)和原生函數(shù)實現(xiàn)綁定到一起,這是最直接,也是最高效的綁定方式。
非裸綁定是指通過某個 JS 和原生的通信的橋梁(evaluate/prompt/postMessage 等等),在此基礎(chǔ)上再封裝和轉(zhuǎn)發(fā)具體的函數(shù)調(diào)用。由于存在中間一層的轉(zhuǎn)發(fā)處理,會有額外的消耗。因此小游戲?qū)ν馓峁┑?WebGL 等接口的實現(xiàn),都采用了裸綁定的方式。直接用原生裸綁定的 API,又會存在以下問題:
原生 API 使用較復(fù)雜
不方便實現(xiàn)更高層次的類綁定
V8 和 JavaScriptCore 的 API 差異很大,兩個平臺需要重復(fù)實現(xiàn)綁定
于是,我們實現(xiàn)了一套通用的綁定庫: jsbinding,公司內(nèi)是開源的,未來計劃對外也開源。
具有如下特點:
簡單易用,支持類綁定
裸綁定,性能高
同時支持 V8 和 JavaScriptCore
支持 node addon 綁定實現(xiàn)
未來甚至計劃提供 WebAssembly 的綁定實現(xiàn),是不是還有點小期待呢?
NodeJs/libuv
安卓客戶端已經(jīng)全面擁抱 node。集成 node runtime 后,擁有了如下能力:
node 內(nèi)置能力(如文件、setTimeout 等)
libuv 異步 IO 處理的能力
node 很多內(nèi)置能力,是通過原生來實現(xiàn)的(node addon),屬于裸綁定,性能較高。有了 libuv 事件驅(qū)動后,可以更加靈活和高效的處理一些異步事件。比如 WebSocket 的回調(diào),之前的處理流程是,在子線程收到 socket 消息后,將消息內(nèi)容通過 JNI 調(diào)用到 Java 層,Java 層再拋到 JS 線程(也是 JVM 線程),回調(diào)到 JS。而如果使用 libuv,可以在子線程通過 uv_async_send 封裝的 ASyncCall 機制,在底層就直接拋到 JS 線程回調(diào)到 JS,避免了中間頻繁的 JNI 調(diào)用和數(shù)據(jù)傳輸?shù)拈_銷。
調(diào)用鏈路優(yōu)化
我們都知道,兩點之間,直線最短。代碼也是一樣,調(diào)用鏈路越短,越直接,中間的開銷就越小。
JsApi 優(yōu)化
1. JsApi 調(diào)用優(yōu)化
首先來看看之前 JsApi 的調(diào)用鏈路:
mark一個 js api 的調(diào)用(WeixinJSCore.invokeHandler),首先會調(diào)用到 C/C++ 統(tǒng)一的回調(diào)函數(shù) voidCallback,然后再通過 JNI 調(diào)用到 Java 的統(tǒng)一處理函數(shù) callVoidJavaMethod。在這個函數(shù)里,需要根據(jù) methodID 從 map 中找到對應(yīng)的 Java Method,然后再通過多次 JNI 調(diào)用 J2V8 各種接口將 js api 的參數(shù)轉(zhuǎn)換為 Java 類型參數(shù),最后再調(diào)用到具體 API 的 Java 實現(xiàn)函數(shù) Invoke。
這個調(diào)用鏈路顯然不是前面提到的裸綁定的實現(xiàn)方法,因為中間還夾了一層 Java 的中轉(zhuǎn)處理層,產(chǎn)生了一些性能消耗。
針對 invokeHandler,縮短調(diào)用鏈路,減少 JNI 調(diào)用優(yōu)化后,流程如下:
mark針對 js 的 WeixinJSCore.invokeHandler 接口提供專門的 C++ 裸綁定接口 InvokeHandler,取出所有參數(shù)后,只需要一次 JNI 調(diào)用到 nativeInvokeHandler,然后調(diào)到具體 API 的 Java 實現(xiàn)函數(shù) Invoke。
除此之外,針對異步 JsApi 調(diào)用,之前的流程是在 java 層拋到另一個線程執(zhí)行。有了 libuv Looper 后,優(yōu)化成在底層起一個 uv 的 worker 線程,通過 ASyncCall 將任務(wù)拋到 worker 線程,這樣 worker 里只需要執(zhí)行同步的 api 流程,流程上簡化了,效率上比拋 Java 層線程更高。
2. JsApi 回調(diào)優(yōu)化
當(dāng)框架層需要觸發(fā) JS 回調(diào)時,之前的做法是拼好一段 JS 字符串然后 evaluate:
evaluateJavascript(String.format("typeof?WeixinJSBridge?!==?'undefined'?&&?"?+"WeixinJSBridge.invokeCallbackHandler(%d,?%s)",callbackId,?data ));這里的本質(zhì)是去調(diào)用 JS 里的統(tǒng)一回調(diào)處理函數(shù) WeixinJSBridge.invokeCallbackHandler,采取了直接執(zhí)行一段 JS 的方法。優(yōu)點是實現(xiàn)簡單,缺點是效率不高。
因為讓 JS 引擎執(zhí)行一段 JS 代碼時,需要先編譯,parse 抽象語法樹,生成 Ignition 字節(jié)碼,甚至啟用到 TurboFan 編譯優(yōu)化器,最后才真正執(zhí)行到想調(diào)用的 JS 函數(shù)。
同時每個回調(diào)都拼一個字符串執(zhí)行,在 JS 引擎內(nèi)部會積攢大量臨時字符串,占用內(nèi)存資源。
優(yōu)化的方法其實也很簡單,就是通過 jsbinding 預(yù)先查找好 WeixinJSBridge.invokeCallbackHandler 函數(shù),在需要回調(diào)這個 JS 函數(shù)時,直接調(diào)用即可。
//?查找到?invokeCallbackHandler?函數(shù)后,保存下來 mm::JSObject?func?=JS_GET_AS(mm::JSObject,?js_bridge,?"invokeCallbackHandler"); js_func_holder_?=?JS_NEW_OBJECT_HOLDER(func);//?...//?當(dāng)需要回調(diào)時,直接調(diào)用 JS_CALL(js_func_holder_->Get(),?nullptr,?nullptr,js_bridge,?callbackId,?data);并行調(diào)用優(yōu)化
開發(fā)者在執(zhí)行某些耗時較重的任務(wù)時,可以使用多線程 Worker,類比標(biāo)準(zhǔn) H5 的 WebWorker。
//?主線程初始化?Worker const?worker?=?wx.createWorker('workers/request/index.js')?//?文件名指定?worker?的入口文件路徑,絕對路徑 //?向?Worker?發(fā)送消息 worker.postMessage({msg:?'hello?worker' })//?workers/request/index.js //?在?Worker?線程執(zhí)行上下文會全局暴露一個?`worker`?對象 worker.onMessage(function?(res)?{console.log(res) })之前的 Worker 有個限制,只能執(zhí)行一些純邏輯運算的代碼,不支持 JsApi 的調(diào)用。這很大程度限制了 Worker 的使用,于是我們也在不斷的擴展 Worker 的能力,增加了音頻、網(wǎng)絡(luò)、文件等能力。
//?Worker?線程 var?audio?=?worker.createInnerAudioContext() audio.src?=?url audio.play()未來 Worker 將會賦予更多能力,提高開發(fā)者并行化處理的效率。
數(shù)據(jù)傳輸優(yōu)化
開發(fā)者在 JS 層的數(shù)據(jù)(ArrayBuffer)需要傳到客戶端底層,同時客戶端底層的數(shù)據(jù)也需要傳到 JS 上層,這中間涉及到數(shù)據(jù)的高效傳輸。在渲染優(yōu)化時,可以通過 wgfx 提供的 createNativeBuffer 接口,創(chuàng)建一塊 JS 和 Naitve 共享的內(nèi)存,雙方可直接讀寫該內(nèi)存而無需額外的傳輸,極大的提高了效率。
NativeBuffer 的共享內(nèi)存?zhèn)鬏敊C制,可以應(yīng)用到多個需要頻繁傳輸數(shù)據(jù)的場景,比如 Camera 傳輸?shù)臄?shù)據(jù)、JS 的 WebGL CommandBuffer 傳輸?shù)鹊取?/p>
還有一種情況是前面提到的 Worker 之間傳輸數(shù)據(jù),如果通過默認(rèn)的 postMessage 來傳輸,效率是非常低的,不利于傳輸較大的 ArrayBuffer 數(shù)據(jù)。為了解決這個問題,我們提供了類似標(biāo)準(zhǔn) H5 的 SharedArrayBuffer 的能力,用來 Worker 之間高效的傳輸數(shù)據(jù)。
//?game.js const?sab?=?wx.createSharedArrayBuffer(2) worker.postMessage({sab })//?worker.js worker.onMessage(function?(res)?{res.sab.lock(()?=>?{setTimeout(()?=>?{res.sab.unlock()},?3000)}) })總結(jié)
小游戲的性能瓶頸,很大程度局限于 JavaScript,而我們所做的各種優(yōu)化,是希望能盡量抹平 JavaScript 本身帶來的性能損耗,接近并向原生性能靠齊,極具困難和挑戰(zhàn)。
在 iOS 上,我們也為讓 JavaScript 擁有 JIT 能力做了深入探索。同時,我們也在 WebAssembly 上也進行了深入的探索和支持,未來有機會再進行分享。
為了小游戲有更好的運行性能,開發(fā)者能更好的發(fā)揮其創(chuàng)意,我們所有的性能優(yōu)化還將持續(xù)不斷的迭代下去。
總結(jié)
以上是生活随笔為你收集整理的微信小游戏背后的技术优化的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: GAIR 2020 工业互联网专场演讲实
- 下一篇: ClickHouse留存分析工具十亿数据