Lynx技術分析-JS引擎擴展設計

JS Binding 技術

Lynx(一個高效的跨平臺框架) 的 JS Binding 技術最主要的目的是搭建一個高效的與 JS 引擎解耦的通訊橋樑,同時具有 JS 引擎切換的能力。該技術經歷了屢次迭代,最終經過抽象的引擎接口層設計,在代碼層面作到對於 JS 引擎的解耦。目前 Lynx 在 Android 端支持 V8 和 JSC 引擎的切換。html

關於 JSC 和 V8 引擎的相關基礎知識能夠瀏覽上一篇文章android

遇到的問題

Lynx 是一個 JS 驅動的跨平臺框架,提供了 JS 調用 Android 和 iOS 等平臺層的渲染能力,同時容許開發者拓展平臺能力,所以在 Lynx 中和 JS 通訊的除了核心 Runtime 層,還包括了處於平臺層的 Module 和 RenderObjectImpl,同時在框架中存在線程間通訊的狀況。結合上述 Lynx 框架的特性,在 JS Binding 迭代時遇到的主要的問題:ios

  1. 代碼解耦:對於不一樣的 JS 引擎的初始化等流程和 Extension (須要定義靜態方法)方式的統一
  2. C++ 對象生命週期管理
  3. 跨線程和跨平臺的參數轉化

設計

總體設計代碼在 runtime 目錄c++

對於跨線程和跨平臺的參數轉化的問題,爲了便於參數在上下游的轉化(JS 與核心 C++ 層的轉化,核心層與平臺層 Android & iOS 的轉化),定義了 LynxValue 做爲通用傳遞參數,並根據不一樣平臺制定 LynxValue 的轉化規則,減小參數在跨層調用時繁瑣的轉化步驟。轉化規則如今包括 JSC 到核心層的 JSCHelper,V8 到核心層的 V8Helper,核心層到 iOS 層的 OCHelper,以及核心層到 Android 層的 JNIHelper。下面的圖能夠看出 LynxValue 流通與不一樣層次。git

主要數據結構github

  • LynxValue 是參數傳遞規則的基類,其中使用了聯合體定義了支持轉化的參數。包括基本數據類型,數組,鍵值對,LynxObject 和 LynxFunction 等。除了 LynxFunxtion 和 LynxObject,其他參數均不能直接和 JS 通訊,僅用於參數轉化,同時支持跨線程跨平臺傳遞。
  • LynxArray 有序的有限個的 LynxValue 的集合,對應 JS 端和平臺層的數組。
  • LynxMap 鍵值對,僅支持字符串做爲 key,對應 JS 端的 Map 或者 Object 和平臺層的鍵值對。
  • LynxFunction 存儲了 JS 端的 function,用於在合適時機回調 JS function。
  • LynxObject 通訊基類,藉助 ClassTemplate 構建與 JS 通訊橋樑的對象(請看後續分析),能夠對 JS 對象進行間接的操做,如 ProtectJSObject 的操做,使 JS 對象脫離 GC。

在 JS 引擎代碼解耦方面,JSC 和 V8 在 JS 原型和 Extension 上的設計都是類似的邏輯,只是在實現的細節上不一致。如在 JSC 中利用 JSClassRef 描述原型上所具備的屬性和方法,同時能夠構造原型鏈,而 V8 中利用 FunctionTemplate 和 PrototypeTemplate 代替;JSC 中使用 JSObjectSetPrivate 接口爲 JS 對象綁定一個 C++ 對象,而 V8 則利用 ObjectTemplate::SetInternalField 方法代替。基於上述特色,Lynx 的 JSBinding 抽象了一層 JS 的原型構造器和方法鉤子的接口,以知足與 JS 的通訊功能。數組

JSVM 是表明 JS 運行的虛擬機,真正的實現文件交由各自引擎實現。安全

JSContext 爲 JS 引擎的控制上下文,同時是一個模板類,其中包含全局對象 Global,對於真正的 V8 和 JSC 的操做由其實現類 V8ContextJSCContext 決定。而與 JS 通訊主要使用對外接口 ClassTemplate 和內部接口 ObjectWrap。數據結構

ClassTemplate 用於構造 JS 原型的模板,經過該模板能夠註冊函數和變量鉤子等(Extension 功能。該對象持有PrototypeBuilder,PrototypeBuilder 由對應的 JS 引擎實現,用於構建 JSC 的 JSClassRef 或者是 V8 的 FunctionTemplate,同時能夠根據原型建立 JS 對象。 ClassTemplate 提供了宏定義幫助定義默認 ClassTemplate 的靜態方法,下面是宏定義的意義和用法:框架

  • DEFINE_CLASS_TEMPLATE_START 默認 ClassTemplate 構建的方法定義的開始
  • REGISTER_PARENT 定義 ClassTemplate 的父親(原型鏈),在 START 和 END 之間使用。
  • EXPOSE_CONSTRUCTOR 在 JS 上下文中暴露該 ClassTemplate 做爲構造器,在 START 和 END 之間使用。
  • REGISTER_METHOD_CALLBACK 向 ClassTemplate 中註冊函數鉤子,在 START 和 END 之間使用。
  • REGISTER_GET_CALLBACK REGISTER_SET_CALLBACK 向 ClassTemplate 中註冊變量鉤子,在 START 和 END 之間使用。
  • DEFINE_CLASS_TEMPLATE_END 默認 ClassTemplate 構建的方法定義的結束。
  • DEFAULT_CLASS_TEMPLATE 獲取默認 ClassTemplate。

defines.h 頭文件含有用於定義 JS 引擎鉤子函數的宏規則,C++ 類須要根據宏定義鉤子函數,並將函數指正註冊到 ClassTemplate 中,同時自身須要有對應的類方法(鉤子函數會進行回調)進行真正實現,才能完成原型的構建。ClassTemplate.h 中經過宏定義提供了快速構建一個與 C++ 對象默認的 ClassTemplate 對象。結合兩個宏定義規則,能夠實現快速構建與 JS 通訊的 C++ 類。下面是 defines.h 中宏定義的意義:

  • DEFINE_METHOD_CALLBACK 用於定義 JS 引擎函數鉤子,DEFINE_GROUP_METHOD_CALLBACK 用於定義帶方法名稱做爲參數的函數鉤子,METHOD_CALLBACK 用於獲取鉤子名稱
  • DEFINE_SET_CALLBACK DEFINE_GET_CALLBACK 用於定義 JS 引擎變量鉤子,SET_CALLBACK GET_CALLBACK 用於獲取鉤子名稱。

自定義類方法鉤子示例:

JS 變量的 Get 鉤子:base::ScopedPtr<LynxValue> Function();

JS 變量的 Set 鉤子: void Function(base::ScopedPtr<jscore::LynxValue>& value);

JS 方法鉤子:base::ScopedPtr<LynxValue> Function(base::ScopedPtr<LynxArray>& array);

ObjectWrap 用於創建 JS 對象和 C++ 對象(這裏指 LynxObject)的關係,即用於管理 C++ 對象生命週期,C++ 對象的生命週期是跟隨 JS 對象(固然 JS 對象只是對 C++ 對象進行引用計數上的增減,確保 C++ 對象在被其餘類引用時能夠被安全釋放或使用)。JS對象和C++對象綁定的時機在 ClassTemplate 建立 JS 對象時,這個時機由 JS 運行上下文決定(在 defines.h 中的鉤子函數中處理),無需開發者關心。

JS Binding 總體運行圖示,在 Lynx 開發中,JS 引擎的具體實現或者參數轉化規則對外無感知,利用 LynxObject 和 LynxValue 就能夠與 JS 通訊,完成 API 調用工做。LynxValue 和 JSValue 的轉化均是在 JSObject 和 LynxObject 相互調用時進行。

實例:定義與 JS 對象 console 關聯的 Console 類,實現 console.log 的函數調用,主要步驟以下

  1. 繼承 LynxObject ,定義被鉤子函數調用的 Log 類方法
  2. 定義須要進行 Extension 的 Log 函數鉤子
  3. 根據 ClassTemplate 提供的宏定義,快速建立默認的 ClassTemplate,在構造函數中傳入默認的 ClassTemplate。
namespace jscore {
    class Console : public LynxObject {
    public:
        Console(JSContext* context);
        virtual ~Console();
        // 定義 JS 引擎函數鉤子回調的類方法
        base::ScopedPtr<LynxValue> Log(base::ScopedPtr<LynxArray>& array);
    };
}
複製代碼
namespace jscore {

    #define FOR_EACH_METHOD_BINDING(V) \ V(Console, Log) 

    // 定義須要進行 Extension 的函數鉤子
    FOR_EACH_METHOD_BINDING(DEFINE_METHOD_CALLBACK)

    // 定義默認的 ClassTemplate
    DEFINE_CLASS_TEMPLATE_START(Console)
        FOR_EACH_METHOD_BINDING(REGISTER_METHOD_CALLBACK)
    DEFINE_CLASS_TEMPLATE_END
    
    // 構造函數中傳入默認的 ClassTemplate
    Console::Console(JSContext* context) : LynxObject(context, DEFAULT_CLASS_TEMPLATE(context)) {
    }

    Console::~Console() {}

    base::ScopedPtr<LynxValue> Console::Log(base::ScopedPtr<LynxArray>& array) {
    	// Print log
        return base::ScopedPtr<LynxValue>(NULL);
    }

}
複製代碼

優缺點

優勢:隔離 JS 引擎代碼,易於切換;無額外消耗的函數鉤子(通訊)實現,比 RN 的通訊更快;快速上手,相比於 Web IDL 沒有學習成本。

缺點:仍然須要在通訊類中手動編寫必定代碼;暫時只知足於和 JS 引擎通訊的功能,相比於 Web IDL 而言功能相對簡單,暫時無涉及多種外部語言。

嘗試

Git 拉 Lynx 工程源碼,根據 How To Build 運行 Android 工程,在 Android 工程根目錄的 gradle.properties 中,經過設置 js_engine_type=v8/jsc 進行 V8 引擎和 JSC 引擎的切換。iOS 僅支持 JSC 引擎。

請持續關注 Lynx,一個高性能跨平臺開發框架。

相關文章
相關標籤/搜索