本節簡要介紹支付寶小程序從 Service Worker 到 V8 Worker 的技術演進過程。前端
衆所周知,支付寶小程序源碼打包完成以後主要分爲兩部分:java
同時,前端框架 APPX 也分爲 Render 部分(af-appx.min.js)和 Worker 部分(af-appx.worker.min.js):android
Service Workergit
Service Worker 由瀏覽器內核提供,設計目的是用於充當 Web 應用程序與瀏覽器之間的代理服務器;Service Worker 運行在獨立的 worker 上下文,所以它不能訪問 DOM。相對於驅動應用的主 JavaScript 線程,它運行在其餘線程中,因此不會形成阻塞。github
可是有一個問題是 Service Worker 的啓動和 Render 部分的啓動是串行的,必須是在 WebView 啓動以後,由 Render 部分的 JS 發起。這對小程序來講就是較大的性能瓶頸。web
WebView Worker小程序
爲了解決 Worker 和 Render 串行初始化和執行帶來的性能問題,小程序團隊嘗試過使用 WebView 來執行 Worker。也就是在啓動小程序的時候同時 new 出兩個 WebView,一個 WebView 用來渲染 Render 部分,另外一個 WebView 專門用來執行 Worker 部分的 JS 腳本。可是專門使用一個 WebView 來執行 Worker 部分的 JS 腳本,無疑是」大材小用「,使用一個 WebView 的資源消耗必然是較高的。api
V8 Worker瀏覽器
Service Worker 的串行初始化會影響小程序啓動性能,WebView Worker 來運行小程序 Worker 代碼又不夠輕量,使用專有 JS 引擎來作 Worker 部分的工做乃是最優選擇,所以 V8 Worker 應運而生。緩存
下圖是小程序 V8 Worker 的基本結構,本文後面繼續詳細描述。
利用 V8 引擎運行 Worker 主要有如下一些優點:
V8 Worker 基礎架構
本節主要介紹了支付寶小程序的 V8 Worker 工程結構、基於 V8 Worker 的小程序架構;同時若是對 V8 引擎不是很熟悉,這裏給出了 V8 的簡要介紹和學習資料連接。
V8 簡介與入門
在介紹 V8 Worker 以前,先簡要了解下 V8 引擎[2]自己。若是對 V8 很熟的大牛請自行跳過。
V8 是 Google 的開源項目,是一個高性能 JavaScript 和 WebAssembly 引擎,應用於用於 Chrome 瀏覽器、Node.js 等項目。學習 V8 的門檻仍是比較高,這裏只給出了閱讀本文所須要知道的 V8 基本概念,以及官方的嵌入式 V8 的 HelloWorld 代碼,同時給出一些學習連接。
嵌入式 V8 基本概念
1 Isolate (隔離)
Isolate 和操做系統中進程的概念有些相似。進程是徹底相互隔離的,一個進程裏有多個線程,同時各個進程之間並不相互共享資源。Isolate 也是同樣,Isolate1 和 Isolate2 兩個擁有各自堆棧的虛擬機實例,且相互徹底隔離。
2 Contexts (上下文)
在 V8 中,一個 context 就是一個執行環境, 它使得能夠在一個 V8 實例中運行相互隔離且無關的 JavaScript 代碼。你必須爲你將要執行的 JavaScript 代碼顯式的指定一個 context。
之因此這樣是由於 JavaScript 提供了一些內建的工具函數和對象,他們能夠被 JS 代碼所修改。好比,若是兩個徹底無關的 JS 函數都在用一樣的方式修改一個 global 對象,極可能就會出現一個意外的結果。
3 Handle(句柄)與 垃圾回收
Handle 提供了一個 JS 對象在堆內存中的地址的引用。V8 垃圾回收器將回收一個已沒法被訪問到的對象佔用的堆內存空間。垃圾回收過程當中,回收器一般會將對象在堆內存中進行移動. 當回收器移動對象的同時,也會將全部相應的 Handle 更新爲新的地址。
當一個對象在 JavaScript 中沒法被訪問到,而且也沒有任何 Handle 引用它,則這個對象將被看成 "垃圾" 對待。回收器將不斷將全部斷定爲 "垃圾" 的對象從堆內存中移除。V8 的垃圾回收機制是其性能的關鍵所在。
Local Handles 保存在一個棧結構中,當棧的析構函數(destructor)被調用時將同時被銷燬。這些 handle 的生命週期取決於 handle scope(當一個函數被調用的時候,對應的 handle scope 將被建立)。當一個 handle scope 被銷燬時,若是在它當中的 handle 所引用的對象已沒法再被 JavaScript 訪問,或者沒有其餘的 handle 指向它,那麼這些對象都將在 scope 的銷燬過程當中被垃圾回收器回收。入門指南中的例子使用的就是這種 Handle。
Persistent handle 是一個堆內存上分配的 JavaScript 對象的引用,這點和 local handle 同樣。但它有兩個本身的特色,是對於它們所關聯的引用的生命週期管理方面。當你但願持有一個對象的引用,而且超出該函數調用的時期或範圍時,或者是該引用的生命週期與 C++ 的做用域不一致時,就須要使用 persistent handle 了。例如 Google Chrome 就是使用 persistent handle 引用 DOM 節點。Persistent handle 支持弱引用,即 PersistentBase::SetWeak,它能夠在其引用的對象只剩下弱引用的時候,由垃圾回收器出發一個回調。
4 Templates(模板)
在一個 context 中,template 是 JavaScript 函數和對象的一個模型。你可使用 template 來將 C++ 函數和數據結構封裝在一個 JavaScript 對象中,這樣它就能夠被 JS 代碼操做。例如,Chrome 使用 template 將 C++ DOM 節點封裝成 JS 對象,而且將函數安裝在 global 命名空間中。你能夠建立一個 template 集合, 在每一個建立的 context 中你均可以重複使用它們。你能夠按照你的需求,建立任意多的 template。然而在任意一個 context 中,任意 template 都只能擁有一個實例。
在 JS 中,函數和對象之間有很強的二元性。在 C++ 或 Java 中建立一種新的對象類型一般要定義一個類。而在 JS 中你卻要建立一個函數, 並以函數爲構造器生成對象實例。JS 對象的內部結構和功能很大程度上是由構造它的函數決定的。這些也反映在 V8 的 template 的設計中, 所以 V8 有兩種類型的 template:
1)FunctionTemplate
一個 Function Template 就是一個 JS 函數的模型. 咱們能夠在咱們指定的 context 下經過調用 template 的 GetFunction 方法來建立一個 JS 函數的實例. 你也能夠將一個 C++ 回調與一個當 JS 函數實例執行時被調用的 function template 關聯起來。
2)ObjectTemplate
每個 Function Template 都與一個 Object Template 相關聯。它用來配置以該函數做爲構造器而建立的對象。
5 Accessors (存取器)
存取器是一個當對象屬性被 JS 代碼訪問的時候計算並返回一個值的 C++ 回調。存取器是經過 Object Template 的 SetAccessor 方法進行配置的。該方法接收屬性的名稱和與其相關聯的回調函數,分別在 JS 讀取和寫入該屬性時觸發。
存取器的複雜性源於你所操做的數據的訪問方式:
6 Interceptors(攔截器)
咱們能夠設置一個回調,讓它在對應對象的任意屬性被訪問時都會被調用。這就是 Interceptor。考慮到效率,分爲兩種不一樣的 interceptor:
7 Security Model(安全模型)
在 V8 中,同源被定義爲相同的 context。默認狀況下,是沒法訪問別的 context 的。若是必定要這樣作,須要使用安全令牌或安全回調。安全令牌能夠是任意值,但一般來講是個惟一的規範字符串。當創建一個 context 時,咱們能夠經過 SetSecurityToken 來指定一個安全令牌, 不然 V8 將自動爲該 context 生成一個。
基於 V8 Worker 的小程序架構
本小節詳細講述 V8 Worker 的小程序架構,分別描述了 Render 部分和 V8 Worker 的 JSAPI 流程細節,以及 Render 和 Worker 直接如何通訊。
單 V8 Context 結構
如上圖所示,在 V8 Worker 的初期,一個小程序佔用一個 V8 Isolate,一個 V8 Isolate 只建立一個 V8 Context。也就是小程序的前端框架 APPX 的代碼 appx.worker.min.js 和小程序的業務代碼 index.worker.js 運行於同一個 V8 Isolate 上的同一個 V8 Context 上。這樣的設計就會存在 JS 安全性問題,業務 JS 代碼能夠經過拼接冒名的形式訪問到爲 APPX 注入的內部 JS 對象和內部 JSAPI,在同一個 V8 Context 中,是沒法隔離開業務 JS 代碼和 APPX 框架 JS 代碼的運行環境的。後面咱們會介紹如何解決這個安全問題。
Render 部分 JSAPI 流程
如上圖所示,Render 和 Nebula 直接的雙向通行是分別經過 Console.log 和 WebView 的 loadUrl[9] 接口進行的。
容器到 Render
容器要加載運行 Render 部分的 JS 腳本,都是經過 WebView 的 loadUrl 進行;WebView 在運行 Render 部分的 JS 腳本(af-appx.min.js 和 index.js)以前,須要提早注入 APPX 框架須要用到的全局 JS 對象,如 window.AlipayJSBridge[10] 等,供 JSAPI 調用使用。
Render 到容器
Render 側到容器的 JSAPI 的調用,本質上是經過 Console.log[11] Web API 實現。
Worker 部分 JSAPI 流程
Worker 到容器
相似於 Render 部分,在初始化 V8 Worker 時,也須要在 V8 Worker 環境中注入 AlipayJSBridge 這個全局 JS 對象,AlipayJSBridge 的定義在 workerjs_v8_origin.js [12]中,workerjs_v8_origin.js[13] 已提早在 V8 Worker 中加載。
同時,咱們已經在 V8 Worker 環境中提早注入了 nativeFlushQueue API,同時綁定了這個 API 的 JAVA 側回調:
這樣 Worker 部分 JSAPI 經過 AlipayJSBridge.call() 調用,最終會回調到容器側的AsyncJsapiCallback() 。
容器到 Worker
JSAPI 在容器側處理完成以後,若是有返回結果,將會返回到 Worker。
Render 和 Worker 通訊
基於容器總線的消息通道
以 Render 到 Worker 發送消息爲例,流程大體爲:
能夠看出,基於容器總線的消息通道,一個消息從 Render 到 Worker 中間須要通過屢次的序列化和反序列化,這是很是耗時的操做;不只在小程序啓動過程當中影響小程序啓動速度,小程序的滑動等交互事件都會有大量的 Worker 和 Render 之間的消息傳遞,因此也會影響幀率。
因而,基於 MessageChannel 的消息通道應運而生。
MessageChannel 容許咱們建立一個新的消息通道,並經過它的兩個 messagePort 屬性發送數據。以下圖所示,MessageChannel 會建立一個管道,管道的兩端分別表明一個 messagePort,都可以經過 portMessage 向對方發送數據,經過 onmessage 來接受對方發送過來的數據。利用 MessageChannel 的特性,render 和 worker 之間的通訊能夠不經過 Nebula 總線,這樣減小了消息的序列化和反序列化。
V8 Worker 接入 JSI
背景
隨着支付寶端以及整個集團使用V8引擎的業務愈來愈多,對 V8 引擎的升級維護工做就愈來愈複雜和重要。每一個業務可能使用不一樣的接口,升級 V8 引擎時都須要從新適配。同時,剛纔前文也提到了,目前 V8 引擎由 UCWebView 內核提供,使用 V8 須要從新進行拷貝。
如何解決這些問題呢?"計算機科學領域的任何問題均可以經過增長一個間接的中間層來解決",因而就誕生了 JSI(JavaScript Interface)。
JSI 簡介
JSI(JavaScript Interface)是對 JavaScript 引擎(V八、JSC 等)進行封裝,給業務方提供基礎的、完整的、穩定的、與具體 JS引擎無關的、向後兼容的 Java API 和 Native API。
JSI 帶來的優點有:
基於 JSI 的 V8 Worker
下圖是基於 JSI 的 V8 Worker 工程結構。對比基於 J2V8[14] 的 V8 Worker 發現,小程序、小遊戲、Cube 等業務只須要經過 JSI 的 Java 接口去加載 V8 引擎便可,JSI 中使用 U4 Linker 加載 libwebviewuc.so,可複用 UC WebView SDK 中的 libwebviewuc.so,且無需拷貝,解決了與 UC WebView 在同一個進程中共存時 libwebviewuc.so 全局變量衝突的問題。JSI 同時提供了 Java 和 C++ 兩種封裝 API,方便業務方接入。
JSI 接入文檔詳細介紹瞭如何快速經過 JSI 來使用 JS 引擎:
V8 Worker 如何解決 JS 安全問題
前文已經介紹,採用單 V8 Isolate 單 V8 Context 結構的 V8 Worker 會存在 JS 安全問題,沒法隔離業務 JS 和前端框架 JS 的運行環境。下面就介紹多 Context 隔離的 V8 Worker 和多 Isolate 隔離多線程 Worker。
多 Context 隔離
下圖描述了多 V8 Context 隔離架構的 V8 Worker。對於同一個小程序,在同一個 V8 Isolate 下,分別爲小程序前端框架腳本(af-appx.worker.minjs)、小程序業務腳本(index.worker.js)和小程序插件[15]腳本(plugin/index.worker.js)建立單獨 APPX Context、Biz Context、Plugin Context(jsi::JSContext 就對應於 v8::Context)。同一個小程序可能會存在多個小程序插件,對於每個插件都會分配一個單獨 V8 Context 運行環境。
如 V8 Context 安全模型[16]所描述,同源即被定義爲 Context,默認狀況下不一樣的 Context 是不能相互訪問的,除非經過 SetSecurityToken 設定安全令牌。正式利用了這一特性,咱們將前端框架、小程序業務和小程序插件的 JS 運行環境進行了安全隔離。
多 Isolate 隔離的多線程 Worker
在小程序中,對於一些異步處理的任務,能夠放置於後臺 Worker 線程去運行,待運行結束後,再把結果返回到小程序主線程,這就是多線程 Worker。
上圖描述了多線程 Worker 的設計框架。小程序 Worker 主線程運行於單獨的 V8 Isolate 上,同時,業務 JS、APPX 框架 JS、插件 JS 會運行屬於各自的 V8 Context 上。同時對於每個 Worker 任務,都會單獨起一個 Worker 線程,建立單獨的 V8 Isolate 和 V8 Context 實例。每個 Worker 任務和小程序主線程中的任務都是相互線程隔離的、Isolate 隔離的。
Isolate 隔離意味着 V8 堆的隔離,所以 Worker 主線程和後臺 Worker 線程,是沒法直接傳遞數據的。Worker 主線程和後臺 Worker 線程要想實現數據傳遞,則須要進行序列化和反序列化(Serialize 和 Deserialize)。序列化即將數據從源 V8 堆上拷貝至 C++ 堆上,反序列化即將數據從 C++ 堆上拷貝至目標 V8 堆上。Worker 主線程和後臺 Worker 線程經過序列化和反序列化的接口 postMessage 和 onMessage 來進行數據傳遞。
JS 引擎能力輸出
支付寶中一些其餘業務如(Native GCanas)想要在 C++ 層得到 JS 引擎能力,同時不想本身費力去從新接入 JS 引擎。這時須要 V8 Worker 具有將小程序的 JS 運行環境對外輸出的能力。V8 Native 插件是其中一個方案。
V8 Native 插件
下圖描述了 V8 Native 插件的框架。設計思路以下:
插件業務經過接入 V8 Native 插件將得到以下能力:
因爲插件業務可以直接得到小程序 JS 的執行環境,所以插件業務必須可信的,不然會帶來安全問題;因此在 V8 Worker java 層須要對插件進行白名單管理和開關控制。
V8 Worker 性能優化
並行初始化
V8 Worker 最初引入的緣由就是爲了解決小程序 Render 和 Worker 串行初始化和執行的問題。前文已經介紹,這裏再也不贅述。
Code Caching
上圖是 V8 code caching 的原理。由於 JS 是 JIT 語言,因此 V8 運行 JS 時須要先解析和編譯,所以 JS 的執行效率一直都是個問題。V8 code caching 的原理是,第一次運行 JS 腳本的時候同時會生成該 JS 腳本的字節碼緩存,並保存在本地磁盤,第二次再運行同一個腳本的時候,V8 能夠利用第一次保存的字節碼緩存重建 Compile 結果,這樣就不須要從新 CompileCode。這樣第二次利用 Code Cache 以後,執行這個腳本將會更快。
V8 Code caching 分爲兩種:
Eager Code caching 生成的緩存將會更全,熱點函數命中率也會更高。同時體積將會更大,所以第二次從磁盤加載緩存時耗時也會更多。V8 官方宣稱 Eager Code caching會比 Lazy Code caching 減小 20%-40% 的 parse 和 compile 的時間。實際上咱們經過實驗發現 Eager Code caching 並不比 UC 目前的 Lazy Code caching 有更好的效果。緣由是緩存的體積對性能影響巨大。可是經過 Trace 分析,使用 Eager Code caching 和沒有使用 cache 相比,JS 執行時間仍是有較大的提高。
相關連接
[1]https://developer.mozilla.org...
[3]https://chromium.googlesource...
[5]https://docs.google.com/prese...
[6]https://docs.google.com/prese...
[7]https://docs.google.com/prese...
[8]https://v8.dev/blog/code-caching
[9]https://developer.android.com...)
[10]https://codesearch.alipay.com...
[11]https://developer.mozilla.org...
[12]https://codesearch.alipay.com...
[13]https://codesearch.alipay.com...
[14]https://github.com/eclipsesou...
[15]https://opendocs.alipay.com/m...
[16]https://v8.dev/docs/embed#sec...
福利來了 | 電子書下載《小程序開發不求人》
本書系統全面地講解了支付寶小程序的開發技術,語言詼諧生動,帶領讀者從零開始全面體驗小程序的開發工具、基礎語法、開發框架、實現過程、快速示例及延展場景研發,深刻淺出幫助讀者快速掌握小程序開發技能。適合對 HTML、CSS 和 JS 有基本瞭解的讀者。
點擊文末」閱讀原文「當即下載