天天看點

揭秘:支付寶小程式 V8 Worker 技術演進

揭秘:支付寶小程式 V8 Worker 技術演進

從 Service Worker 到 V8 Worker

本節簡要介紹支付寶小程式從 Service Worker 到 V8 Worker 的技術演進過程。

衆所周知,支付寶小程式源碼打包完成之後主要分為兩部分:

  • 第一部分負責小程式的視圖展示,打包産物為 index.js,我們稱為 Render 部分
  • 第二部分負責小程式的業務邏輯、視圖更新等,打包産物為 index.worker.js,我們稱為 Worker 部分

同時,前端架構 APPX 也分為 Render 部分(af-appx.min.js)和 Worker 部分(af-appx.worker.min.js):

  • Render部分(index.js 和 af-appx.min.js)運作在 UCWebView 或 SystemWebView 上
  • Worker 部分(index.worker.js 和 af-appx.worker.min.js)運作于 Service Worker[1] 上

Service Worker

Service Worker 由浏覽器核心提供,設計目的是用于充當 Web 應用程式與浏覽器之間的代理伺服器;Service Worker 運作在獨立的 worker 上下文,是以它不能通路 DOM。相對于驅動應用的主 JavaScript 線程,它運作在其他線程中,是以不會造成阻塞。

揭秘:支付寶小程式 V8 Worker 技術演進

但是有一個問題是 Service Worker 的啟動和 Render 部分的啟動是串行的,必須是在 WebView 啟動之後,由 Render 部分的 JS 發起。這對小程式來說就是較大的性能瓶頸。

揭秘:支付寶小程式 V8 Worker 技術演進

WebView Worker

為了解決 Worker 和 Render 串行初始化和執行帶來的性能問題,小程式團隊嘗試過使用 WebView 來執行 Worker。也就是在啟動小程式的時候同時 new 出兩個 WebView,一個 WebView 用來渲染 Render 部分,另一個 WebView 專門用來執行 Worker 部分的 JS 腳本。但是專門使用一個 WebView 來執行 Worker 部分的 JS 腳本,無疑是”大材小用“,使用一個 WebView 的資源消耗必然是較高的。

揭秘:支付寶小程式 V8 Worker 技術演進

V8 Worker

Service Worker 的串行初始化會影響小程式啟動性能,WebView Worker 來運作小程式 Worker 代碼又不夠輕量,使用專有 JS 引擎來做 Worker 部分的工作乃是最優選擇,是以 V8 Worker 應運而生。

下圖是小程式 V8 Worker 的基本結構,本文後面繼續較長的描述。

揭秘:支付寶小程式 V8 Worker 技術演進

利用 V8 引擎運作 Worker 主要有以下一些優勢:

  • 能夠解決 Render 和 Worker 串行初始化和運作的問題,WebView 和 V8 引擎可并行初始化、可并行執行 Render 和 Worker 部分的 JS 腳本
  • 能夠提供 JS 安全運作環境,隔離架構 JS 和業務 JS
  • 易于給小程式注入 JS 對象,綁定 JSAPI
  • 能夠支援更豐富的資料類型,如 ArrayBuffer 等
  • 能夠擴充 Worker 能力,提供小程式插件、多線程 Worker 等功能
  • 能夠充分利用 V8 引擎的能力做性能優化,如 V8 CodeCache 等
  • 能夠給小程式以外的業務提供 JS 引擎能力,如 V8 Native 插件
  • 能夠自定義 JS 引擎運作參數
揭秘:支付寶小程式 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:

  • 屬性名攔截器:當通過字元串形式的屬性名通路時調用。比如在浏覽器中使用 document.theFormName.elementName 進行通路。
  • 屬性索引攔截器:當通過屬性的下标/索引通路時調用。比如在浏覽器中使用 document.forms.elements[0] 進行通路。

7 Security Model(安全模型)

在 V8 中,同源被定義為相同的 context。預設情況下,是無法通路别的 context 的。如果一定要這樣做,需要使用安全令牌或安全回調。安全令牌可以是任意值,但通常來說是個唯一的規範字元串。當建立一個 context 時,我們可以通過 SetSecurityToken 來指定一個安全令牌, 否則 V8 将自動為該 context 生成一個。

學習資料

  • 官方文檔 [4]
  • Ignition: An Interpreter for V8 [5]
  • Ignition: Jump-starting an Interpreter for V8 [6]
  • V8: Hooking up the Ignition to the Turbofan [7]
  • V8 Code Caching [8]

基于 V8 Worker 的小程式架構

本小節詳細講述 V8 Worker 的小程式架構,分别描述了 Render 部分和 V8 Worker 的 JSAPI 流程細節,以及 Render 和 Worker 直接如何通信。

揭秘:支付寶小程式 V8 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 中加載。

AlipayJSBridge = {
  //xxxxx
  call: function (func, param, callback) {
     nativeFlushQueue(func, viewId, JSON.stringify(msg), extraData);
  }
  //xxxxx
}           

同時,我們已經在 V8 Worker 環境中提前注入了 nativeFlushQueue API,同時綁定了這個 API 的 JAVA 側回調:

mV8Runtime.registerJavaMethod(new AsyncJsapiCallback(this), "__nativeFlushQueue__");           

這樣 Worker 部分 JSAPI 通過 AlipayJSBridge.call() 調用,最終會回調到容器側的AsyncJsapiCallback() 。

容器到 Worker

JSAPI 在容器側處理完成之後,如果有傳回結果,将會傳回到 Worker。

Render 和 Worker 通信

基于容器總線的消息通道

以 Render 到 Worker 發送消息為例,流程大緻為:

  • Render 側發送 postMessage 消息,此時消息需要經過一次序列化轉成字元串。
  • WebChromeClient onConsoleMessage 攔截到消息,反序列化成 JSONObject 并發送到容器總線 bridge.sendToNative(event) 。
  • 容器總線進行事件分發。
  • worker 插件攔截到 postMessage 事件,并發送到 worker。
  • V8Worker 将消息反序列化成 string,并轉成 JS 資料類型,傳到 Worker 所在的 V8Context。
  • workerjs_v8_origin.js 中_invokeJS 函數被調用,至此,Worker 已收到來自 Render 的消息。

基于 MessageChannel 的消息通道

可以看出,基于容器總線的消息通道,一個消息從 Render 到 Worker 中間需要經過多次的序列化和反序列化,這是非常耗時的操作;不僅在小程式啟動過程中影響小程式啟動速度,小程式的滑動等互動事件都會有大量的 Worker 和 Render 之間的消息傳遞,是以也會影響幀率。

于是,基于 MessageChannel 的消息通道應運而生。

MessageChannel 允許我們建立一個新的消息通道,并通過它的兩個 messagePort 屬性發送資料。如下圖所示,MessageChannel 會建立一個管道,管道的兩端分别代表一個 messagePort,都能夠通過 portMessage 向對方發送資料,通過 onmessage 來接受對方發送過來的資料。利用 MessageChannel 的特性,render 和 worker 之間的通信可以不通過 Nebula 總線,這樣減少了消息的序列化和反序列化。

揭秘:支付寶小程式 V8 Worker 技術演進

V8 Worker 接入 JSI

背景

揭秘:支付寶小程式 V8 Worker 技術演進

随着支付寶端以及整個集團使用V8引擎的業務越來越多,對 V8 引擎的更新維護工作就越來越複雜和重要。每個業務可能使用不同的接口,更新 V8 引擎時都需要重新适配。同時,剛才前文也提到了,目前 V8 引擎由 UCWebView 核心提供,使用 V8 需要重新進行拷貝。

如何解決這些問題呢?"計算機科學領域的任何問題都可以通過增加一個間接的中間層來解決",于是就誕生了 JSI(JavaScript Interface)。

JSI 簡介

JSI(JavaScript Interface)是對 JavaScript 引擎(V8、JSC 等)進行封裝,給業務方提供基礎的、完整的、穩定的、與具體 JS引擎無關的、向後相容的 Java API 和 Native API。

揭秘:支付寶小程式 V8 Worker 技術演進

JSI 帶來的優勢有:

  • 與具體 JavaScript 引擎無關的、各平台通用 JSI API
  • 實作了 Inspector、Code Cache、JS Timer 等通用的功能,能讓業務方更加集中精力地關注于業務方面的開發,縮短開發周期
  • JSI 負責處理 JS 引擎版本相容性問題,接入業務無感覺
  • JSI 直接使用 UC 核心中的 libwebviewuc.so,不需要進行 V8 拷貝
  • JSI 采用支付寶和 UC 共建的形式,JSI 接入層下沉到 Ariver 工程,通過 Ariver 輸出到集團各業務

基于 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 引擎:

  • Java 和 Native 側的初始化
  • 建立 JSEngine(對應于 v8::isolate)
  • 建立 JSContext(對應于 v8::Context)
  • 如何通過 Java/C++ 接口注入 JS 對象(全局常量、全局函數、全局通路器)
  • 如何執行 JS 腳本
  • Trace 分析、Timer 等
揭秘:支付寶小程式 V8 Worker 技術演進

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 運作環境進行了安全隔離。

揭秘:支付寶小程式 V8 Worker 技術演進

多 Isolate 隔離的多線程 Worker

在小程式中,對于一些異步處理的任務,可以放置于背景 Worker 線程去運作,待運作結束後,再把結果傳回到小程式主線程,這就是多線程 Worker。

揭秘:支付寶小程式 V8 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 Worker 中增加一層 C++ 插件代碼,定義 Native 插件的接口,加載業務的動态連結庫并管理插件。
  • 将小程式 JS 運作環境(基于 JSI 的 C++ 接口,jsi::JSEngine、jsi::JSContext)通過插件接口暴露給插件業務方,業務方即可獲得小程式JS運作環境,友善添加自定義的 JS 對象,綁定自定義 JSAPI。
  • V8 Worker 将小程式生命周期事件,通過插件接口通知給業務方。
  • 同時給插件業務暴露 PostTask 接口,允許插件業務将任務放到小程式的 JS 線程去執行。

插件業務通過接入 V8 Native 插件将獲得如下能力:

  • 獲得小程式生命周期事件
  • 獲得小程式 JS 執行環境
  • 在小程式 JS 線程執行任務
  • 通路小程式的 JS 對象,JSAPI
  • 注入自定義 JS 對象,綁定自定義的 C++ 實作 JSAPI

由于插件業務能夠直接獲得小程式 JS 的執行環境,是以插件業務必須可信的,否則會帶來安全問題;是以在 V8 Worker java 層需要對插件進行白名單管理和開關控制。

揭秘:支付寶小程式 V8 Worker 技術演進

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 分為兩種:

  • Lazy Code caching:隻将跑過的熱函數生成 code caching
  • Eager Code caching:将整個JS腳本都生成 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 執行時間還是有較大的提升。

揭秘:支付寶小程式 V8 Worker 技術演進
相關連結

[1]

https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API

[2]

https://v8.dev/docs

[3]

https://chromium.googlesource.com/v8/v8/+/branch-heads/6.8/samples/hello-world.cc

[4]

[5]

https://docs.google.com/presentation/d/1OqjVqRhtwlKeKfvMdX6HaCIu9wpZsrzqpIVIwQSuiXQ/edit#slide=id.ge4ef702cb_2_67

[6]

https://docs.google.com/presentation/d/1HgDDXBYqCJNasBKBDf9szap1j4q4wnSHhOYpaNy5mHU/edit#slide=id.g1357e6d1a4_0_58

[7]

[8]

https://v8.dev/blog/code-caching

[9]

https://developer.android.com/reference/android/webkit/WebView#loadUrl(java.lang.String)

[10]

https://codesearch.alipay.com/source/xref/Android_wallet_master/android-phone-nebula-git/nebula/js/h5_bridge.js?r=78c30345

[11]

https://developer.mozilla.org/en-US/docs/Web/API/Console/log

[12]

https://codesearch.alipay.com/source/xref/Android_wallet_master/android-ariver/js/workerjs_v8_origin.js?r=b59d7f92

[13]

[14]

https://github.com/eclipsesource/J2V8

[15]

https://opendocs.alipay.com/mini/plugin/plugin-introduction

[16]

https://v8.dev/docs/embed#security-model

福利來了 | 電子書下載下傳《小程式開發不求人》

本書系統全面地講解了支付寶小程式的開發技術,語言诙諧生動,帶領讀者從零開始全面體驗小程式的開發工具、基礎文法、開發架構、實作過程、快速示例及延展場景研發,深入淺出幫助讀者快速掌握小程式開發技能。适合對 HTML、CSS 和 JS 有基本了解的讀者。

識别下方二維碼,或點選文末”

閱讀原文

“立即下載下傳:

揭秘:支付寶小程式 V8 Worker 技術演進