
文章圖片

文章圖片

文章圖片

文章圖片

文章圖片

文章圖片

文章圖片
作者:京東科技 杜強強
前言在Roma跨端方案中 , JS虛擬機是框架的核心 , 負(fù)責(zé)執(zhí)行動態(tài)化的JS代碼 。 在Android平臺采用了基于V8的J2V8 , iOS平臺則使用了系統(tǒng)自帶的JSCore , 而在HarmonyOS中 , 由于業(yè)界無類似的框架 , 我們需要自行實現(xiàn)以確保核心基礎(chǔ)能力的完整 。鴻蒙虛擬機的開發(fā)經(jīng)歷了從最初 ArkTs2V8 到JSVM + Roma新架構(gòu)方案 。 在此過程中 , 我們實現(xiàn)了完整的鴻蒙版的“J2V8”和 基于系統(tǒng)JSVM的JS虛擬機框架 , 解決了JS引擎庫移植、多語言通信能力、多類型數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換等眾多挑戰(zhàn) 。 本文將從實現(xiàn)的各個階段過程出發(fā) , 探討在實踐中遇到的問題及解決方案 。
一、鴻蒙版 “J2V8”虛擬機實現(xiàn) - ArkTs2V8ArkTs2V8框架依賴V8引擎 ,鴻蒙前期交叉編譯資料少 , V8官方也未有HarmonyOS端編譯方式 。 因此在這過程中 ,我們采取初期使用QuickJS引擎(C語言開發(fā) , 代碼少 , 移植方便) ,后期自編譯V8完成后替換QuickJS ,保證快速驗證跨端前期技術(shù)調(diào)研方案以及其他依賴項基礎(chǔ)能力的開展 。自編譯V8 通過學(xué)習(xí)交叉編譯相關(guān)技術(shù) , 摸索式逐步解決編譯期間這種報錯 , 完成V8虛擬機移植 。
ArkTs2V8 架構(gòu)借鑒了Android J2V8(動態(tài)化-J2V8文章中講述了具體原理及實踐)的實現(xiàn)原理 。J2V8為針對V8的 Java實現(xiàn) , 采用最直接的方式在Java中訪問V8原始值 , 因此具備較高的性能 。在HarmonyOS中 , 采用V8作為JS引擎 ,JSI作為通信層完成設(shè)計 。
??
1、引入JSI考慮到跨端框架的未來發(fā)展 , 雖然通過C++ 能夠直接與V8交互 , 但這種方式不利于虛擬機代碼的共享和擴展 。 因此Roma框架引入JSI , 以增強代碼的可擴展性 , 促進(jìn)更有效的代碼共享 , 并實現(xiàn)更靈活的虛擬機集成 。
JSI(JavaScript Interface) , 輕量級 , 通用且同步的JavaScript接口 ,通過JSI , JS代碼可以直接與C++原生代碼通信 。
有了JSI層對虛擬機的封裝 , Roma框架開發(fā)者無需在關(guān)心虛擬機底層能力 ,同時也可以自由切換引擎 , 比如使用V8 , QuickJS、JSVM等 規(guī)范了數(shù)據(jù)格式 , 統(tǒng)一為JSIValue 。
2、API與框架設(shè)計原理接口設(shè)計采用和J2V8 類似的設(shè)計 , 支持多虛擬機實例方式 。
實現(xiàn)原理:
1、本地接口: 使用 napi 使用創(chuàng)建橋梁 ,完成本地代碼調(diào)用Quick引擎函數(shù) 。
2、C++數(shù)據(jù)綁定:在C++層面, 定義虛擬機交互操作的相關(guān)函數(shù) , 完成V8引擎相關(guān)API 來執(zhí)行JS代碼、 處理JS對象和執(zhí)行虛擬機相關(guān)的操作 。
3、JSIRuntime: 在C++層面引入JSI概念 , 通過完成JSIRuntime - QuickJSRuntime & V8Runtime ,完成虛擬機層通信能力 。
4、虛擬機對象的定義及封裝:根據(jù)JS數(shù)據(jù)類型 , 定義ArkTS數(shù)據(jù)結(jié)構(gòu) , 包括基本數(shù)據(jù)類型、JSObject、JSArray、JSFunction 。 ArkTS側(cè) 類型對象持有C++ JSIValue 對象指針 , 當(dāng)執(zhí)行具體能力時 , 通過napi 傳遞指針 , 完成具體功能的調(diào)用 。簡單來說 , 相當(dāng)于ArkTS JS對象代理C++ 虛擬機數(shù)據(jù)對象 。
5、內(nèi)存管理: ArkTs2V8負(fù)責(zé)管理ArkTS與JSValue 之間的內(nèi)存交互 。 其中C++側(cè)完成JSValue對象的創(chuàng)建、引用持有與銷毀 。ArkTS數(shù)據(jù)對象中定義對象釋放函數(shù) ,數(shù)據(jù)使用完后 , 由ArkTS調(diào)用釋放內(nèi)存 。
ArkTs2V8架構(gòu)設(shè)計支持虛擬機多實例 ,單個虛擬機的創(chuàng)建過程時由 ArkTs通過JSEngine發(fā)起創(chuàng)建JSRuntime虛擬機實例創(chuàng)建 , 經(jīng)過napi , 在C++環(huán)境創(chuàng)建JSRuntime引擎實例及引用 ,并完成環(huán)境Context及global的初始化 ,同時創(chuàng)建ArkTs JSRuntime對象 , 代理C++虛擬機對象JSRuntime(QuickJSRuntime or V8Runtime) 并綁定指針引用 。
初始化過程:
??
V8Runtime實現(xiàn)
??
3、JS、JSI、JSRuntime 關(guān)系
JSRuntime (QuickJSRuntime or V8Runtime) 是 JS運行時環(huán)境 。 一個 JSRuntime 通常包括一個或多個引擎 , JSI 可以看作是連接 JS 代碼和 JSRuntime 的橋梁 。 通過 JSI , 開發(fā)者可以更直接地與 JSRuntime 交互 , 實現(xiàn)原生功能的調(diào)用和管理 。
4、部分過程剖析ArkTs2V8實現(xiàn)的過程中 , 最基礎(chǔ)的兩個功能原理:JSObject對象的創(chuàng)建與獲取、原生方法的注入 ,這兩個能力的實現(xiàn)可以擴展到其他大多數(shù)API功能實現(xiàn)上 。
1、JSObject對象及獲取對象數(shù)據(jù)過程 。通過JSRuntime 發(fā)起接口的調(diào)用 , 通過napi , 根據(jù)對象類型在C++側(cè)創(chuàng)建對象的JSValue對象及象指針引用 ,并將引用指針綁定至ArkTS對象 , 完成對象的創(chuàng)建 。
??
2、 JS虛擬機注入原生方法ArkTS方法到JS虛擬機中 , 主要實現(xiàn)原理:
將ArkTs的方法 和 目標(biāo)注冊對象指針 生成MethodDescriptor方法描述對象 ,通過functionID將對象存儲在當(dāng)前JSContext環(huán)境中 。通過napi 發(fā)起在C++側(cè)代理函數(shù)HostFunction的創(chuàng)建 , 并綁定ArkTs的方法的引用 。進(jìn)入到JSI內(nèi)部 , 創(chuàng)建方法代理HostFunctionProxy 對象 , 綁定代理方法HostFunction及守護(hù)函數(shù)Finalizer v8::External 將HostFunctionProxy與 JS環(huán)境對象(V8對象) 關(guān)聯(lián)起來 , 生成V8 Function,此時V8函數(shù)會與HostFunctionProxy生命周期綁定 。簡單來說相當(dāng)于ArkTS callback , 傳遞至C++ , C++創(chuàng)建JSI Callback并綁定ArkTS callback ,JSI Callback 設(shè)置到HostFunctionProxy中 , HostFunctionProxy 通過 v8::External與 JS環(huán)境綁定 。
【鴻蒙跨端實踐-JS虛擬機架構(gòu)實現(xiàn)】當(dāng)JS觸發(fā)該該函數(shù)時 , 通過v8::External綁定HostFunctionProxy這層關(guān)系 , HostFunctionProxy中JSI Callback會收到JS環(huán)境的響應(yīng)消息 , 在通過綁定的ArkTs的方法 通過napi接口返回至ArkTS中 , 最終ArkTS收到方法響應(yīng) 。
這種代理函數(shù)的實現(xiàn) ,初次學(xué)習(xí)可能比較復(fù)雜 , 但整個過程實際是多個對象間引用的持久化和不同數(shù)據(jù)對象的交換 ,大致過程圖如下:
4、問題及挑戰(zhàn)1、 數(shù)據(jù)對象的內(nèi)存管理手動內(nèi)存管理 。ArkTs2V8 負(fù)責(zé)管理ArkTS與V8之間的內(nèi)存交互中 , ArkTs發(fā)起對象的創(chuàng)建和銷毀 。整個內(nèi)存的管理是基于手動管理 , 需使用方用完后及時關(guān)閉 , 避免內(nèi)存泄露 。這種設(shè)計模式下 , 使用者操作不當(dāng)極為容易造成內(nèi)存泄露 , 并且使用也較為不便 。
針對這問題 , 在后續(xù)的迭代設(shè)計中 , 將內(nèi)存管理升級為自動內(nèi)存管理的方式 。JS為單線程執(zhí)行 , 單方法片段或一些邏輯中 , 如果有了調(diào)用開始時機和結(jié)束調(diào)用時機 ,通過開始時記錄當(dāng)前時刻后開始創(chuàng)建的對象 , 在調(diào)用結(jié)束時刻對記錄的對象進(jìn)行統(tǒng)一的內(nèi)存釋放 , 類似于標(biāo)記垃圾回收 , 完成內(nèi)存的統(tǒng)一管理 。 借助Roma框架中對虛擬機層的封裝 , 做到了內(nèi)存自動管理 。
2、 跨語言性能問題基于ArkTs2V8 的API實現(xiàn) , 在原生、JS環(huán)境中 , 無法直接使用對方的數(shù)據(jù)類型 , 二者之間數(shù)據(jù)類型需要轉(zhuǎn)換 。JS到原生的過程中 , ArkTs2V8中目前提供的API僅可以獲取當(dāng)前層級的JS對象數(shù)據(jù) , 子對象數(shù)據(jù)需要通過遞歸遍歷從JS環(huán)境中一一獲取 。 因此解析的過程中需要頻繁的通過C++讀取V8 , 當(dāng)數(shù)據(jù)量較大時通常比較耗時 。 拿常用的網(wǎng)絡(luò)模塊來說 , 接口下發(fā)的業(yè)務(wù)接口數(shù)據(jù)至少都在幾K甚至幾十K , 轉(zhuǎn)為JS對象在中端性能手機上 會有幾十ms的耗時 , 這對單線程模式的JS環(huán)境來說影響時巨大的 。
ArkTS 、C++ 跨語言通信性能我們可以采取類似于Roma Android的通信次數(shù)壓縮策略 , 或者使用JSON序列化來減少跨語言交互的性能損耗 ,但無論用哪種都僅是從行為上規(guī)避跨語言的性能 , 而無法徹底解決 。
3、 線程管理問題ArkTS基于TS語言 , 由于語言特性 , ArkTS線程隔離 , 那么對于ArkTs2V8這種接口設(shè)計并不友好 。JS線程需要在ArkTS開啟獨立worker JS線程 , 收發(fā)JS消息 , 線程間的隔離 , 涉及再次序列化數(shù)據(jù)影響性能 。
基于問題2、3 以及對框架未來的思考 , Roma鴻蒙端決定采用新的方案: 框架C++化 , 框架邏輯實現(xiàn)全部放在native側(cè) ,虛擬機實現(xiàn)全部切C++ , C++側(cè)完成線程管理 , ArkTS不在承擔(dān)線程和邏輯任務(wù) 。這種既提升了解決了問題 , 提升框架性能 , 也為今后框架移植其他平臺打好基礎(chǔ) 。
二、基于JSVM虛擬機實現(xiàn) (Roma新架構(gòu))1、鴻蒙JSVM在V8移植上 , 從短期看雖然我們初步掌握了V8交叉編譯移植技術(shù) , 但從穩(wěn)定性、兼容性、維護(hù)成本、包大小等維度看 ,采用系統(tǒng)內(nèi)置虛擬機有巨大的長期收益 。年初Roma框架與華為專家多次溝通交流 , 最終HarmonyOS將V8內(nèi)置到了操作系統(tǒng) ,Q2我們實現(xiàn)了第三個JSRuntime - JSMVRuntime ,至此鴻蒙動態(tài)化架構(gòu)修改趨于穩(wěn)定 。
JSVMRuntime:
??
2、新架構(gòu)思路 - Roma架構(gòu)C++化新架構(gòu)的設(shè)計思路SDK核心邏輯整體C++側(cè)實現(xiàn) ,這樣在底層引擎與核心流程之間可以直接c++通信 , 線程間上與其他端保持相同 - 三線程模型JS線程 +UI線程 + 耗時計算線程 。 通過C++ PThread完成線程管理 ,從而避免跨語言、ArkTS線程隔離帶來的多種性能損耗 。在數(shù)據(jù)結(jié)構(gòu)設(shè)計上 ,JS數(shù)據(jù)采用JSI::Value ,與其他線程數(shù)據(jù)相互交互時 ,統(tǒng)一使用folly完成 。另外將虛擬機層下沉 , 對外提供JSExecutor ,功能開發(fā)時框架開發(fā)者無需關(guān)心虛擬機層的實現(xiàn) 。
虛擬機方法與對象的注入上 ,通過HostObject代理對象能力的雙邊映射 , 原生模塊直接與JS 同步或異步交互 ,從而縮短了流程鏈路 。
框架大致原理:
??
3、過程遇到的1、JSVM字符串引用問題JSVMRuntime實現(xiàn)期間 , 字符串無法創(chuàng)建對象引用 。JSI的設(shè)計中將字符串作為 pointer 自定義指針類 , 通過指針地址訪問 ,與其相同的還有對象 , 方法 。在許多語言中字符串都作為一種特殊的類型(非基本數(shù)據(jù)類型) ,例如在C++中 , 字符是一種基本數(shù)據(jù)類型 , 但是字符串不是 , 字符串由字符組成 ,V8引擎亦如此 。V8中通常使用v8::String來創(chuàng)建JS字符串 ,我們可以對齊進(jìn)行持久化引用 。
而JSVM中 OH_JSVM_CreateReference 無法針對字符串類型創(chuàng)建引用 ,字符串的持久化需從JSVM_Value從copy出來通過智能指針或者new內(nèi)存的方式進(jìn)行存儲 , 這種copy持久化的方式會造成字符串內(nèi)存兩份(JSVM一份 , 自己存一份) ,實際開發(fā)中大量的字符串類型轉(zhuǎn)換 , 這樣會造成內(nèi)存占比過高 。
為此 ,經(jīng)過與華為專家多次交流溝通 , 最終將字符串歸為引用類型 , 可通過OH_JSVM_CreateReference持久化引用 , 修改后的方式如下:
2、HostObject代理對象實現(xiàn)HostObject 是JS對象 , 提供與原生直接通信的方式 。相當(dāng)于 native 在 JS的代理對象 , 雙向映射 , 原生模塊直接與JS 同步或異步交互 ,在一些功能實現(xiàn)上可以縮短流程鏈路 ,在JS中可直接調(diào)用C++的對象 。在動態(tài)化中 ,模塊的實現(xiàn)采用的就是HostObject能力 ,框架層實現(xiàn)模塊代理對象及橋通信層面的雙向通信過程 。比如登錄模塊 , 在ArkTS側(cè)封裝模塊的API , 通過C側(cè)的HostObject映射 , 可以在JS中直接調(diào)用登錄模塊的登錄 , 退出登錄等能力 。HostObject的實現(xiàn) , 雖然在框架層面相比于喬通道的方式更加復(fù)雜 , 但對于復(fù)雜邏輯流程和交互鏈路 ,基礎(chǔ)開發(fā)可以更注重于功能邏輯 。
HostObject 實現(xiàn)過程較為復(fù)雜 ,但我們可以將過程拆分 , 通過對象管理 + 代理函數(shù)的方式將過程簡化 。首先對象的管理直接JSRuntime中持久化即可 , Roma中采用智能指針 , 那么就剩下代理函數(shù) , 前面我們講了JS中注入方法里面包括了代理函數(shù)的實現(xiàn)原理 , 采用類似的思路來完成HostObject 。
HarmonyOS提供的JSVM API最初僅支持代理函數(shù)的創(chuàng)建 ,而我們需要是創(chuàng)建代理對象 , 對象中可以有任意方法 , 僅通過代理函數(shù)方式無法滿足任意方法的需求 , 為此通過在JS中注入代理對象腳本實現(xiàn) , 通過Proxy代理的方式 , 將get、set等代理對象的方法通過代理函數(shù)的方式返回 , 這種情況下 , 我們的函數(shù)數(shù)量就被簡化成了get、set及一些固定的方法 。通過這些方法做代理轉(zhuǎn)接 , 調(diào)用到C++對象方法 , 借助JSI::Value的包裝 , 將具體結(jié)果返回 。
JS代理腳本部分代碼:
大致實現(xiàn)過程:
示例 - 基于HostObject Console能力實現(xiàn)
三、總結(jié)0到1實現(xiàn)鴻蒙版“j2v8”、“JSRuntime” 讓我們更加了解引擎實現(xiàn)中的各種細(xì)節(jié)和一些難點問題的解決 。一些方案的實現(xiàn) , 也可以延展到其他(非虛擬機)場景 。Roma 框架C++ ,讓Roma框架走向技術(shù)深水區(qū) ,為今后capi、未來技術(shù)做好了基礎(chǔ) , 旨在帶來更優(yōu)的性能和更好的用戶體驗 。
推薦閱讀
- 鴻蒙NEXT獲人民日報肯定,中國自研操作系統(tǒng)迎來“星河璀璨”
- 華為純血鴻蒙國慶后公測,麒麟990已內(nèi)測,幾千萬老用戶沸騰
- 鴻蒙生態(tài)1年走完別人17年的路,每年投入60億激勵開發(fā)者創(chuàng)新
- 釘釘鴻蒙版正式開放用戶測試
- 華為的鴻蒙系統(tǒng)要崩了?手機廠商集體觀望,央視直擊核心問題!
- 鴻蒙NEXT再迎好消息:新老數(shù)據(jù)無縫遷移,已進(jìn)入公測倒計時階段
- 華為Mate70 系列將搭載 “純血” 鴻蒙,麒麟 9100 雙版本性能狂飆
- 鴻蒙崛起:華為重塑PC操作系統(tǒng)!
- 華為官宣,不支持安卓應(yīng)用的純血鴻蒙終于來了
- 鴻蒙生態(tài)邁進(jìn)新時代,誓師大會呼吁政企事業(yè)單位開發(fā)鴻蒙原生版本
