2020, where JavaScriptCore to go?
如何優(yōu)化 JavaScriptCore
從我接觸 iOS 開發(fā)開始鲤拿,和 JS 有關的動態(tài)化場景已經起起伏伏好幾次了晦炊,這些年 JavaScriptCore 從只是用來做 bridge叠萍,到 RN,JSPatch昼钻。作為 iOS 上唯一可用的 JS 虛擬機袁余,JavaScriptCore 確實承載了不少技術的輝煌,但是蘋果已經長達 5 年沒有更新它了冷守。2020 年了刀崖,JavaScriptCore 該何去何從? 從 Flutter 出來之后并齐,Dart 的出現(xiàn)讓我們認識到笙瑟,動態(tài)化的方案尤其是虛擬機這塊,真的該動一動了噪漾。
注意:我這里所說的 JavaScriptCore 是指 iOS 自帶的 JavaScriptCore framework, JavaScriptCore 本身分幾個版本充活,WK 用的叫 Nitro蜂莉,蘋果一直在優(yōu)化蜡娶,但我們無法直接使用
JavaScriptCore,越來越雞肋的存在
我非常理解為什么谷歌要選用 Dart 來做開發(fā)語言映穗,除了要維護自己公司的生態(tài)外窖张,JavaScriptCore 的性能根本無法滿足 Flutter 自繪 UI 的方案。對于 Flutter 團隊來說 JavaScriptCore 就是雞肋蚁滋,棄了就意味和龐大的前端生態(tài)割裂宿接,喪失系統(tǒng)的原生支持,但繼續(xù)使用實在是難受辕录,蘋果對 JavaScriptCore 的態(tài)度幾乎讓人絕望睦霎,故意做了很多使用的限制,讓人有種在破輪子上造新車的感覺踏拜。
JavaScriptCore 的幾大劣勢
性能差
把下面的代碼片段放在不同的 JS 引擎測試性能碎赢。
! function () {
function caculate(x) {
var sin = Math.sin(x);
var cos = Math.cos(x);
return Math.pow(sin, 2) + Math.pow(cos, 2);
}
var data = new Array(1000);
for (var i = 0; i < data.length; i++) {
data[i] = Math.PI * Math.random();
}
var ret = new Array(data.length);
const start = new Date();
for (var i = 0; i < data.length; i++) {
ret = caculate(data);
}
const end = new Date();
console.log("all caculte cost " + (end - start));
}();
結果如下:
V8 | Ntiro | JavaScriptCore |
---|---|---|
87ms | 271ms | 591ms |
可以看到就算是 WK 使用的 Nitro,性能也要比 V8 差一倍左右速梗,更別說閹割版的 JavaScriptCore 了肮塞,V8 的性能是它的七八倍。要是和 Dart 這種支持 AOT 的語言相比更是難以望其項背姻锁。
不支持 JIT
WK 使用的 Nitro 會根據(jù)函數(shù)或循環(huán)執(zhí)行的次數(shù)枕赵,利用 OSR 使用不同優(yōu)化級別的機器碼,而 V8 更激進位隶,會優(yōu)先考慮做 JIT 編譯拷窜。可惜 JavaScriptCore framework 閹割了 JIT涧黄,只靠它的 LLINT 解釋器解釋執(zhí)行篮昧。JIT 的作用是非常明顯的,如果在 JS 做骨骼動畫這種比較復雜的計算笋妥,有 JIT 的話小游戲幀率能保持在 30 幀左右懊昨,而沒有 JIT 只能再 4 幀左右。
不支持 asm.js 和 wasm
asm.js 是 JavaScript 的一個高度優(yōu)化的子集春宣,asm.js 來源于 Emscripten 編譯器項目酵颁。Emscripten 實現(xiàn)了 C/C++編譯成 JavaScript,輸出結果就是 asm.js月帝。
asm.js 的特點是變量是靜態(tài)類型躏惋,且用一個 TypedArray 管理內存,帶來的好處是執(zhí)行性能更好嚷辅。當解釋器遇到 asm.js 代碼時簿姨,可以解釋成更為高效的機器碼。
雖然 asm.js 是編譯器輸出的結果簸搞,但是了解其規(guī)則是可以手寫出來相關代碼的款熬。由于 JavaScriptCore 不支持 JIT深寥,我本來想重寫小游戲的 JS 基礎庫,把高頻調用的一些函數(shù)改成 asm贤牛, 但沒想到蘋果連 asm.js 都不給支持惋鹅。
比如這個方法:
function asmCaculate(array) {
'use asm'
var int1 = array[0] | 0;
var int2 = array[1] | 0;
var int3 = array[2] | 0;
var int4 = array[3] | 0;
var float1 = +(array[4]);
var float2 = +array[5];
var float3 = +array[6];
var float4 = +array[7];
return +Math.exp((int1 - int2 + int3 - int4) | 0) + +Math.exp(+(float1 - float2 + float3 - float4));
}
在支持 asm 的 JS 引擎,耗時會比普通版本少個 10%左右殉簸,但在 JavaScriptCore 上反而會更高闰集。因為 JavaScriptCore 把 asm 降級處理了,由于代碼長度比普通版本長般卑,反而解釋起來更耗時了……
Wasm 是 asm.js 的進階版武鲁,直接將 C/C++轉成二進制格式的類匯編代碼,Wasm 對前端來說非常重要蝠检,有了 Wasm沐鼠,瀏覽器就可以對接大量已有的 C++庫,并且擁有遠超 JS 版本的性能√舅現(xiàn)在已經有了不少游戲使用了 Wasm饲梭,從 Unity 發(fā)來的 Demo 來看,性能還是不錯的焰檩,瀏覽器不再只能玩簡單游戲憔涉。
Wasm 也是 Emscripten 的產物,但無法手寫析苫,只能靠編譯生成兜叨,在 JS 里靠 WebAssembly 接口加載。
雖然 JavaScriptCore 有 WebAssembly 接口衩侥,但被閹割了国旷,一實例化就失敗,無法生成對應的 module茫死,坑爹的是文檔也不提示跪但,就這么霸氣,直接底層 API 封堵璧榄,我說你至少在 JS 把 API 抹掉也行啊吧雹!
調試不方便
JavaScriptCore 的調試只能通過 Safari骨杂,但你經常用的話就會發(fā)現(xiàn),總會有一些坑爹的小毛残劬怼:連不上手機的 JSContext搓蚪,打不開 TimeLine,TimeLine 不顯示堆棧等丁鹉。我現(xiàn)在每次查耗時妒潭,都得用筆記本編包去調試悴能,iMac 長年 TimeLine 不顯示堆棧。
此外雳灾,最好不要開自動打開 JSContext inpector漠酿,因為開著 inpector JSContext 是不會釋放的,對應的資源都泄漏谎亩,容易碰到奇怪的內存問題炒嘲。
最坑的是,如果你用 Safari 斷點調試 JSContext匈庭,那么 JS 的執(zhí)行線程會變夫凸!這在多線程的環(huán)境下簡直是讓人崩潰,尤其是小游戲這種阱持,渲染模塊對線程非常敏感夭拌,所以最好是當 JS 環(huán)境穩(wěn)定了再開 JSContext inspector,或者不要在多線程模式下開衷咽。
自帶的一些坑
JavaScriptCore 還有一些非常隱蔽的坑:
所有接口底層都會加鎖
JavaScriptCore 同一時間只有一個線程能夠訪問虛擬機鸽扁,所以是線程安全的。但這意味著所有進入虛擬機的接口都會加鎖(實際上絕大部分接口都會進虛擬機)兵罢,只有當退出虛擬機才會解鎖献烦,這樣才能支持虛擬機被并發(fā)地調用。
這會有兩個潛在的影響:
想做多線程的話只能用多個虛擬機來實現(xiàn)卖词,但不同虛擬機之間傳遞數(shù)據(jù)會比較麻煩
由于有隱含的 JSLock巩那,所以要特別小心死鎖,尤其是主線程和輔助線程之間要盡量理清關系此蜈,一個虛擬機盡量只在一個線程使用即横。
創(chuàng)建虛擬機自動在當前線程創(chuàng)建 RunLoop
當虛擬機被初始化時,它會自動在當前線程創(chuàng)建一個自己的 RunLoop裆赵,定時去做一些回調东囚,最要命的是它要進入虛擬機,會有加鎖操作战授,而文檔沒有任何說明页藻。
具體表現(xiàn)是,如果你在主線程創(chuàng)建了 JSContext植兰,就算后期只在輔助線程使用份帐,主線程依然會有一個 JS 的 RunLoop 定時回調,并且會給主線程加鎖楣导,如果這時剛好你的輔助線程需要同步主線程废境,就直接死鎖了。這種死鎖和業(yè)務代碼關系不大,查起來讓人摸不著頭腦噩凹。
JavaScriptCore 性能優(yōu)化的手段
JavaScriptCore 還是有一些優(yōu)化手段的巴元,雖然沒有 JIT,但項目還得繼續(xù)驮宴,性能還得優(yōu)化……
比如我們可以借鑒 asm.js 的優(yōu)化方式:
- 變量是靜態(tài)類型
- 利用 TypedArray 作為堆逮刨,傳遞數(shù)據(jù),管理內存
- 沒有 GC
1 需要解釋器兼容幻赚,我們肯定是沒辦法了禀忆。但 2、3 還是可以作為一個優(yōu)化的方向落恼。
此外還有兩點:
- JSLock 也很討厭箩退,單線程使用時,加解鎖的性能白白損失了佳谦。
- 提高 JS-Native 的交互效率戴涝,提高單位時間的 JS-Native 的數(shù)據(jù)吞吐量
上面這些就是我的主要優(yōu)化思路,大致介紹下我是如何實現(xiàn)的钻蔑,希望能有所幫助啥刻。
batch command
減少每一幀的 JS-Native 交互次數(shù),合并 JS-Native 之間傳遞的數(shù)據(jù)咪笑。只在必須時才做 JS-Native 的交互可帽,避免兩個語言環(huán)境切換造成的性能損耗。
將數(shù)據(jù)存儲在一個 TypedArray 中窗怒,TypedArray 自創(chuàng)建后底層內存地址就不會變了映跟,JS 和 Native 都可以從中高效讀取合并的數(shù)據(jù)。
-
JS 調 Native
將 JS 的指令扬虚、參數(shù)壓縮成一行一行的數(shù)字努隙,寫入 TypedArray 里,當需要 Native 執(zhí)行時辜昵,通知 Native 讀取數(shù)據(jù)荸镊,調用真正的函數(shù)。圖中綠色部分是會進 JS 虛擬機的操作堪置,會有潛在的加解鎖躬存。
-
Native 調 JS
和上一條類似的原理,這里用我做手勢優(yōu)化的流程圖表示舀锨,手勢數(shù)據(jù)量大岭洲,且相對高頻,Naive 往 TypedArray 寫數(shù)據(jù)雁竞,幀末通知 JS 取出數(shù)據(jù)做處理钦椭。
Avoid JSLock
通過閱讀 JSCore 的源碼,發(fā)現(xiàn) JS 的 Number 在生成時碑诉,會把值編碼到它的地址里彪腔,解析時也是靠解碼地址來解值,可以自己實現(xiàn)這個過程避免 JSLock进栽,除此之外 JS 里的 undefined德挣,null 都是固定值。TypedArray 也有個好處快毛,初始化后它底層的地址不會改變格嗅,可以靠地址偏移還高效去數(shù)據(jù)。
所以唠帝,優(yōu)化思路是這樣的:
- JSNumber 可以自己構造屯掖,不用經過虛擬機,干掉所有的
JSValueMakeNumber
襟衰,JSValueToNumber
贴铜,JSValueMakeNull
等。 - 因為 JSNumber 不會被 GC瀑晒,且傳遞相對高效绍坝,只需要編解碼地址,所以 JSObject 我們可以設置一個 JSNumber 作為句柄苔悦,JS 和 Native 靠這個句柄從緩存中取對象轩褐,不用經過 JS 虛擬機
- 如果是一批 JSNumber 數(shù)據(jù),就將它們放入 TypedArray玖详,這樣可以避免傳遞過多零散數(shù)據(jù)
Less Garbage collection
在 JS 層把介,對于高頻使用的對象,使用緩存來避免頻繁的 GC竹宋。尤其是要關注一些比較占內存的對象比如 Array劳澄,Canvas 等,在 Native 的 GC 回調蜈七,也要及時清理紋理秒拔、文件等資源,因為 JavaScriptCore 是按照當前設備的內存壓力來判斷是否 GC 的飒硅。
Seperate JS thread
使用 JavaScriptCore 的項目一般是要動態(tài)化執(zhí)行 Native 邏輯砂缩,絕大多數(shù)情況下 JS-Native 這個流程是在一個線程完成的。
但是如果 JS 的邏輯很復雜三娩,性能壓力很大庵芭,可以考慮把 JS 的執(zhí)行線程和 Native 的執(zhí)行線程分開,二者只在 JS 需要同步獲取信息時才做同步雀监,否則就一直異步派發(fā)數(shù)據(jù)給 Native双吆。
這有點像系統(tǒng)底層渲染驅動的實現(xiàn)思路眨唬,CPU 接受到渲染指令,存入 CPU command queue好乐,等待系統(tǒng)調度在合適的時機發(fā)送給 GPU command queue匾竿,最終的 GPU 執(zhí)行時機是異步的。
小游戲渲染和 JS 耗時較大蔚万,我把 JS 和渲染抽成兩個獨立的線程:tt.js.thread, tt.render.thread岭妖,各自做對應的工作,UI工作放主線程反璃,其他耗時操作靠GCD派發(fā)昵慌。這樣就提高了單位時間內 JS-Native 的數(shù)據(jù)吞吐量,從而提高幀率淮蜈。
雖然這樣可以解決單線程的性能瓶頸斋攀,但是實際的實現(xiàn)難度非常大,所以放在最后梧田。因為 OpenGL蜻韭、JavaScriptCore 對多線程非常不友好,要保證它們在多線程環(huán)境下沒有問題真的太難了柿扣。
尤其要注意雖然JavaScriptCore的接口都是線程安全的肖方,但JSObject不是線程安全的。如果JSObject/JSValue在其他線程使用未状,要注意延長它們的生命周期俯画,因為在使用時可能會碰到虛擬機GC。
多線程要考慮好實現(xiàn)方案司草,盡量用最簡單的架構艰垂,同時要注意對線程敏感的接口,而且就算設想的很好埋虹,也要做好心理準備去面臨成噸的 Bug……
JavaScriptCore 未來會怎樣猜憎?
如果蘋果未來依然不更新 JavaScriptCore,不支持 JIT搔课、Wasm胰柑,那么 JavaScriptCore 就無法再支持新技術的出現(xiàn)了。
Flutter 給了大家一種新思路爬泥,Dart 實現(xiàn)了一種 JIT 結合 AOT 開發(fā)的體驗柬讨。未來有可能出現(xiàn)支持 TS 的虛擬機,這樣就是大殺器了袍啡。
但還是期待蘋果能改進下對JavaScriptCore的支持政策踩官,畢竟系統(tǒng)原生的包增量小,有獨立進程境输。2020年了蔗牡,至少先給個JIT颖系?