Lynx技術分析-JS引擎擴充套件技術基礎

hxxft發表於2018-04-16

背景

Lynx 作為一個基於 JavaScript 語言(後續簡稱 JS )的跨平臺開發框架,與 JS 的通訊是"與生俱來"的,框架和 JS 引擎打交道是必不可少的能力。JS 引擎提供了 Extension 功能,提供接入方間接和 JS 通訊的橋樑,Lynx 的 JS Binding 正是基於這個能力進行了封裝,構建一套基礎的 JS API,將能力開放給前端開發者。

當前主流瀏覽器基本都擁有自己的 JS 引擎,在當前移動端最為流行的 JS 引擎屬 V8 和 JavascriptCore (別名 Nitro,後續簡稱 JSC),Lynx 框架圍繞這兩個引擎打造高效的 JS Binding。

JS Binding 最主要的目的是利用 JS 引擎和 JS 通訊,開放底層框架能力,也可以稱它為 JS Bridge,它決定了 JS 和框架層通訊的速度。Lynx 早期版本為了快速實現高效通訊,依賴 V8 引擎(後續簡稱 V8),使用的是“純粹”的 Extension 方式,即依照 V8 的擴充方式實現了 JS Binding,建立一套 JS API ,在 Android 系統上首先實現了渲染層的平臺擴充。

Lynx 以這種方式在早期快速實現可靠高效的通訊能力。但當 Lynx 把平臺擴充到 iOS 時,由於 V8 無法在 iOS 平臺使用,JS Binding 必須把 V8 切換成 JSC ,所有關於 V8 的類和函式定義以及初始化流程,均要替換成 JSC 的標準。第一個 JSC 版本的 JS Binding 是基於 JSC iOS 標準實現的。而第二個 JSC 版本的 JS Binding 是基於純 JSC 標準實現的,這次改動的目的是希望能統一 Android 和 iOS 的底層 JS 引擎。

本文主要介紹主流 JS 引擎使用姿勢及 Lynx 中 JS Binding 的記憶體管理方式,作為 JS Binding 技術演進分析的鋪墊。

JS 引擎使用姿勢

在瞭解 Lynx 中 JS Binding 技術的做法前,先了解一下 V8 和 JSC 在初始化和 Extension 方面的標準實現,從中發現兩個引擎的異同,當掌握了基礎的用法之後,能更好的理解 Lynx 中 JS Binding 的發展路線。下面 Extension 擴充以 example 物件為例。

example.h 標頭檔案,這個類定義了即將暴露給 JS 端的 example 物件所具有的介面,包括 TestStatic 和 TestDynamic 方法及變數 num 的設定和獲取。

class Example {
public:
    Example();
    virtual ~Example();
	
	void TestStatic();
    void TestDynamic();
    
    int num();
    void set_num(int num);
    
private:
    void Initialize();
};
複製程式碼

在具體實現程式碼中,主要功能是建立 JS 上下文,建立 example 的 JS 物件,靜態註冊 testStatic 方法和 num 變數,動態註冊 testDynamic 並暴露到上下文中。完成後可以通過在 JS 端使用如下程式碼訪問到 example c++ 物件的介面。

example.testStatic();
example.testDynamic();
example.num = 99;
console.log(example.num);
複製程式碼

接下來將分析兩個引擎中的實現程式碼,包括環境初始化和 Extension 方式,程式碼主要關注以下點

  • 如何初始化環境及執行上下文
  • 如何關聯 c++ 物件和 JS 物件
  • 如何建立物件,並註冊到上下文中
  • 如何向在 JS 引擎物件原型中靜態註冊變數和方法的鉤子
  • 如何向在 JS 引擎物件中動態註冊方法鉤子
  • 如何銷燬虛擬機器

靜態註冊指的是對 JS 的原型 prototype 設定屬性、方法及鉤子函式,從持有該原型的建構函式建立的物件均有設定的方法和屬性及鉤子函式。

動態註冊指直接對 JS 物件設定方法的鉤子函式,僅有被設定過的物件才擁有被設定的方法。動態註冊屬性鉤子函式的方式在 JS 引擎中暫時沒有提供直接的方式.

V8 初始化和 Extension 方式

example_v8.cc 檔案,以下為 V8 Extension 示例工程部分程式碼,完整程式碼請看附錄 。整體流程總結如下:

  1. 初始化 V8 的環境

    V8::InitializeICUDefaultLocation(argv[0]);
    V8::InitializeExternalStartupData(argv[0]);
    v8::Platform* platform = v8::platform::CreateDefaultPlatform();
    V8::InitializePlatform(platform);
    v8::V8::Initialize();
    複製程式碼
  2. 建立 global 物件模板,據此建立 JS 執行上下文 context,從 context 中獲取 global 物件

    // 建立isolate
    v8::Isolate* isolate = v8::Isolate::New(create_params);
    v8::Isolate::Scope isolate_scope(isolate);
    v8::HandleScope handle_scope(isolate);
    // 建立global 物件模板
    v8::Local <v8::ObjectTemplate> global_template = v8::ObjectTemplate::New(isolate);
    // 建立 JS 執行上下文 context
    v8::Local <v8::Context> context = v8::Context::New(isolate, nullptr, global_template);
    v8::Context::Scope context_scope(context);
    //  context 中獲取 global 物件
    v8::Local<v8::Object> global = context->Global();
    複製程式碼
  3. 建立 example 物件的建構函式模板,在建構函式模板中獲取原型模板,並設定靜態方法和變數的鉤子

    // 建立 example 的建構函式模板, 使用該 c++ 類的初始化函式作為引數(函式鉤子),初始化構造器函式模
    // 板。即當呼叫建構函式建立物件時,會呼叫該鉤子函式做構造處理
    v8::Local<v8::FunctionTemplate> example_tpl = v8::FunctionTemplate::New(isolate);
    // 設定建構函式模板的類名
    example_tpl->SetClassName(V8Helper::ConvertToV8String(isolate, "Example"));
    // 設定內部關聯 c++ 物件的數量為 1
    example_tpl->InstanceTemplate()->SetInternalFieldCount(1);
    // 設定建構函式模板中的原型模板的對應函式名的鉤子
    example_tpl->PrototypeTemplate()->Set(V8Helper::ConvertToV8String(isolate, "testStatic"), v8::FunctionTemplate::New(isolate, TestStaticCallback));
    // 設定建構函式模板中的原型模板的屬性的 Get 和 Set 鉤子方法
    example_tpl->PrototypeTemplate()->SetAccessor(V8Helper::ConvertToV8String(isolate, "num"), GetNumCallback, SetNumCallback);
    複製程式碼

    用於靜態註冊的函式鉤子,包括 testStatic 方法鉤子和 num 的 get / set 鉤子

    // example.testStatic() 呼叫時對應的 c++ 函式鉤子
    static void TestStaticCallback(const v8::FunctionCallbackInfo <v8::Value> &args) {
        Example* example = static_cast<Example*>(args.Holder()->GetAlignedPointerFromInternalField(0));
        example->TestStatic();
    }
    
    // console.log(example.num) 呼叫時對應觸發的 c++ 鉤子函式
    static void GetNumCallback(v8::Local<v8::String> property, const PropertyCallbackInfo<Value>& info) {
        Example* example = static_cast<Example*>(args.Holder()->GetAlignedPointerFromInternalField(0));
        int num = example->num();
        info.GetReturnValue().Set(v8::Number::New(isolate, num));
    }
    
    // example.num = 99 時會觸發該的 c++ 函式鉤子
    static void SetNumCallback(v8::Local<v8::String> property, v8::Local<v8::Value> value, const PropertyCallbackInfo<void>& info) {
        if (value->IsInt32()) {
            Example* example = static_cast<Example*>(args.Holder()->GetAlignedPointerFromInternalField(0));
        	example->set_num(value->ToInt32())
        }
    }
    複製程式碼
  4. 根據函式模板建立 example 物件,關聯對應 c++ 物件,動態註冊方法鉤子

    // 在函式模板中獲取可呼叫的函式
    v8::Local<v8::Function> example_constructor = example_tpl->GetFunction(context).ToLocalChecked();
    // 呼叫函式的建立物件的方法,建立 JS 引擎的 example 物件
    v8::Local<v8::Object> example =
        example_constructor->NewInstance(context, 0, nullptr).ToLocalChecked();
    // 關聯 JS 引擎物件和 c++ 物件
    handle->SetAlignedPointerInInternalField(0, this);
    // 動態註冊函式鉤子
    v8::Local<v8::Function> dynamic_test_func = v8::FunctionTemplate::New(TestDynamicCallback, args.Data())->GetFunction();
    v8::Local<v8::String> dynamic_test_name = v8::String::NewFromUtf8(isolate, "testDynamic", v8::NewStringType::kNormal).ToLocalChecked();
    dynamic_test_func->SetName(dynamic_test_name);
    example->Set(dynamic_test_name, dynamic_test_func);
    複製程式碼

    用於於動態註冊的 testDynamic 的函式鉤子

    // example.testDynamic() 呼叫時對應的 c++ 函式鉤子
    static void TestDynamicCallback(const v8::FunctionCallbackInfo <v8::Value> &args) {
        Example* example = static_cast<Example*>(args.Holder()->GetAlignedPointerFromInternalField(0));
        example->TestDynamic();
    }
    複製程式碼
  5. 將 example 物件作為變數新增到 global 的屬性中

    v8::Local<v8::String> example_v8_str = v8::String::NewFromUtf8(isolate, "example", v8::NewStringType::kNormal).ToLocalChecked()
    global->Set(context, example_v8_str, example).FromJust();
    複製程式碼
  6. 如何銷燬虛擬機器 對於普通的銷燬步驟來說,v8引擎對於虛擬機器的銷燬分為銷燬 Context 和銷燬 Isolate ,一般v8的 context 會使用 v8::Persistent<v8::Context> 持有,在呼叫 v8::Persistent 的 Reset 方法之後,當前 context 中使用擴充套件方式註冊的物件可能不會被完全回收,因此需要自己手動進行回收

JSC 初始化和 Extension 方式

example_jsc.cc 檔案,以下為 JSC Extension 示例工程部分程式碼,完整程式碼請看附錄。整體流程總結如下:

  1. 初始化 JSC 的環境

    JSContextGroupRef context_group = JSContextGroupCreate();
    複製程式碼
  2. 建立 global 類定義,據此建立 global 類,根據 global 類建立 JS 執行上下文 context,從 context 中獲取 global 物件

    JSClassDefinition global_class_definition = kJSClassDefinitionEmpty;
    JSClassRef global_class = JSClassCreate(&global_definition);
    JSContextRef context = JSGlobalContextCreateInGroup(context_group, global_class);
    JSObjectRef global = JSContextGetGlobalObject(context);
    複製程式碼
  3. 建立 example 類定義,向類定義設定靜態方法和變數的鉤子

    // 定義將要 Extension 的靜態方法,其中包含函式鉤子
    static JSStaticFunction s_examplle_function_[] = {
        {"testStatic", TestStaticCallback, kJSClassAttributeNone},
        { 0, 0, 0 }
    };
    // 定義將要 Extension 的變數,其中包含 get 和 set 函式鉤子
    static JSStaticValue s_example_values_[] = {
        {"num", GetNumCallback, SetNumCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
        { 0, 0, 0, 0 }
    };
    // 建立 example 類的定義
    JSClassDefinition example_class_definition = kJSClassDefinitionEmpty;
    // 設定類的對應函式名和引數名的鉤子
    example_class_definition.staticFunctions = s_console_function_;
    example_class_definition.staticValues = s_console_values_;
    // 設定類的名稱
    example_class_definition.className = "Example";
    複製程式碼

    用於靜態註冊的函式鉤子,包括 testStatic 方法鉤子和 num 的 get / set 鉤子

    // example.test() 呼叫時對應的 c++ 函式鉤子
    static JSValueRef TestStaticCallback(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) {
        // 獲取 JS 引擎物件中持有的 c++ 物件
        Example* example = static_cast<Example*>(JSObjectGetPrivate(object));
        example->TestStatic();
    }
    
    // console.log(example.num) 呼叫時對應觸發的 c++ 鉤子函式
    static JSValueRef GetNumCallback(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef* exception) {
        Example* example = static_cast<Example*>(JSObjectGetPrivate(object));
        int num = obj->num();
        return JSValueMakeNumber(ctx, num);
    }
    
    // example.num = 99 時會觸發該的 c++ 函式鉤子
    static bool SetNumCallback(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef value, JSValueRef* exception) {
        Example* example = static_cast<Example*>(JSObjectGetPrivate(object));
        obj->set_num(JSValueToNumber(ctx, value, NULL));
    }
    複製程式碼
  4. 根據類定義建立類,根據類建立 example 物件,關聯對應 c++ 物件,動態註冊方法鉤子

    // 建立 JS 引擎的類
    JSClassRef example_class_ref = JSClassCreate(&example_class_definition);
    JSObjectRef example = JSObjectMake(context, example_class_ref, NULL);
    // 關聯 c++ 物件和 JS 引擎物件
    JSObjectSetPrivate(context, example, this)
    JSClassRelease(example_class_ref);
    // 動態註冊函式鉤子
    JSStringRef dynamic_test_func_name = JSStringCreateWithUTF8CString("testDynamic");
    JSObjectRef dynamic_test_func = JSObjectMakeFunctionWithCallback(context, dynamic_test_func_name, TestDynamicCallback);
    JSObjectSetProperty(context, example, dynamic_test_func_name, dynamic_test_func, kJSPropertyAttributeDontDelete, NULL);
    JSStringRelease(dynamic_test_func_name);
    複製程式碼

    用於於動態註冊的 testDynamic 的函式鉤子

    // example.testDynamic() 呼叫時對應的 c++ 函式鉤子
    static JSValueRef TestDynamicCallback(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) {
        Example* example = static_cast<Example*>(JSObjectGetPrivate(object));
        example->TestDynamic();
    }
    複製程式碼
  5. 將 exmaple 物件作為變數新增到 global 的屬性中

    JSStringRef example_str_ref = JSStringCreateWithUTF8CString("example");
    JSObjectSetProperty(context, global, example_str_ref, example, kJSPropertyAttributeDontDelete, NULL);
    JSStringRelease(example_str_ref);
    複製程式碼
  6. 如何銷燬虛擬機器 JSC引擎中對於虛擬機器的銷燬相對比較簡單,只需要通過呼叫 JSGlobalContextRelease 和 JSContextGroupRelease 來分別對 Context 和 Context Group 即可,記憶體中使用擴充套件方式註冊的物件都會在銷燬過程中呼叫 Finalize 回撥

關於執行緒安全

JS的 context 並非是執行緒安全的,因此一個 context 不能在多個執行緒之間共享,以避免遇見未知錯誤。對於應用來說可能需要使用多個 JS Context,而使用多個 Context 的方式有兩種,獨佔式和共享式。

獨佔式,所謂獨佔式是指一個 Context 使用一個執行緒進行處理 共享式,所謂共享式是指多個 Context 使用一個執行緒進行處理,以v8為例,在切換 Context 的時候需要設定v8::Isolate::Scope 和 v8::Context::Scope ,可以仿造下面示例通過巨集定義來處理

#define MakeCurrent(_isolate, _context) \
v8::Isolate* isolate = _isolate == NULL ? v8::Isolate::GetCurrent() : _isolate; \
v8::Isolate::Scope isolate_scope(isolate); \
v8::HandleScope handleScope(isolate); \
v8::Context::Scope context_scope(v8::Local<v8::Context>::New(isolate, _context));
複製程式碼

小結

在上述的幾個示例中,可以看出在建立物件和註冊靜態方法和變數上,V8 和 JSC 具有各自的 API 和變數命名特點,方法中的引數型別也是完全不一致的,V8 和 JSC 在這一層面上具有極大的差異。這也導致了前述在替換 Lynx 的 JS 引擎時所耗費的成本。

然而兩個引擎的整體流程是一致的,同時 API 和引數型別在概念上也是一致的,其原因是 V8 和 JSC 都遵循 JS 的規範。相同點例如:

  • JSC 和 V8 基本一致的初始化和建立物件流程(同等概念的建構函式,程式碼中沒展示)
  • JSC 中表示的字串 JSStringRef 和 V8 中同一概念的 v8::Local<v8::String>
  • JSC 中用於設定物件屬性的 JSObjectSetProperty 方法,對等於 V8 中的 v8::Local<v8::object>->Set 方法

當對兩個引擎的 API 具有一定熟練度,同時對於 JS 已經有一定的掌握,對於兩個引擎在使用上的異同,能有更好的理解。

JS Binding 中物件生命週期控制

JS 引擎具有記憶體管理機制,在建立 JS Binding 時,不可避免的要建立本地物件(c++ 物件或者平臺物件)與 JS 引擎物件的關係,恰當的物件關係能保證程式在記憶體上的穩定,這需要使用到 JS 引擎的記憶體管理相關知識以及其提供可以控制物件在 GC 機制上的行為的介面。

一切由 JS 引擎提供或建立的物件的生命週期管理需要由其內部的 GC 機制把控,JS 引擎提供了兩個介面管理 JS 物件在 GC 機制上的行為,一個是持久化操作幫助 JS 物件脫離 GC 管理,一個是使 JS 物件迴歸 GC 管理。JS 物件在其生命週期內所出現的行為均可以監聽(加鉤子),例如 JS 物件的初始化和析構監聽。V8 引擎和 JSC 引擎涉及的知識和介面均不相同,但是在概念上是一致的,下面看一下兩個平臺上的區別。

V8 引擎監聽和管理物件生命週期的方法

V8 引擎在使用上會經常出現 Handle(控制程式碼)的用法,這是引擎對於其物件訪問和管理的方式,Handle 提供在堆上的 JS 物件位置的引用。當一個 JS 物件在 JS 上已經無法訪問,同時沒有任何的控制程式碼指向他,則被認為可被 GC 回收,這是引擎提供控制程式碼的意義。

在 V8 引擎中,在 GC 執行階段,垃圾收集器會經常移動堆上的物件,同時會更新控制程式碼內容使其指向 JS 物件在堆上更新後的位置。

Local handle(區域性控制程式碼)和 Persistent handle(持久控制程式碼)是經常使用到的其中兩種控制程式碼型別。

Local handle 被存放在棧上面,它的生命週期僅存在於一個控制程式碼域中(handle scope),當程式跳出函式塊,控制程式碼域析構,區域性控制程式碼也隨之被釋放,指向 JS 物件的控制程式碼數量隨之減少。

Handle scope 好比一個容器(棧),當初始化之後,它會收集在這期間建立的區域性控制程式碼,當被析構之後,所有的區域性控制程式碼將被移除,觸發區域性控制程式碼的析構。

Persistent handle(持久控制程式碼)和區域性控制程式碼一樣提供了一個引用指向堆上分配的 JS 物件,但對於其引用的生命週期管理與區域性控制程式碼不一樣。持久控制程式碼存放在全域性區,因此生命週期不受區域性區域塊的影響,能夠在其的生命週期內在多個函式中多次使用,既 JS 物件脫離 GC 管理。持久控制程式碼可以通過 Persistent::New方法由區域性控制程式碼生成,也可以通過 Local::New 方法生成區域性控制程式碼。可以通過 Persistent::SetWeak 方法進行弱持久化,同時也可以通過 Persistent::Reset 方法去持久化。

弱持久化,設定了弱持久化則 Persistent handle 的 JS 物件會當僅剩下該弱持久控制程式碼指向 JS 物件,GC 收集器將會回收並觸發被設定的監聽函式。

去持久化,釋放持久控制程式碼對於堆上的物件的引用

Local handle 和 Persistent handle 的轉化方式

void TestHandle() {
    v8::Isolate* isolate = v8::Isolate::GetCurrent();
    // 下面的程式碼會建立區域性控制程式碼,需要使用一個 handle scope 來管理
    HandleScope handle_scope(isolate);
    // 建立一個 JS Array 物件,返回的是一個區域性控制程式碼
    v8::Local<v8::Array> local_array = v8::Array::New(isolate, 3);
    // 將區域性控制程式碼轉為持久控制程式碼
    v8::Persistent<v8::Array> persistent_array = v8::Persistent<v8::Array>::New(isolate, local_array);
    // 將持久控制程式碼轉為區域性控制程式碼
    local_array = v8::Local<v8::Object>::New(persistent_array);
    // 將持久控制程式碼去持久化
    persistent_array.Reset();
    // 當函式塊結束後,區域性控制程式碼將被析構, JS Array 物件也在未來某個事件被 GC
}
複製程式碼

將持久控制程式碼進行弱持久化的方式如下,在弱持久化的 API 中提供了 JS 物件被 GC 時的監聽回撥的設定。

static void WeakCallback(const v8::WeakCallbackInfo<ObjectWrap>& data) {
    v8::Isolate *isolate = data.GetIsolate();
    v8::HandleScope scope(isolate);
    // data.GetParameter() 是上述 MakeWeak 時傳進來的引數 this
}

void MakeWake() {
    persistent_array_.SetWeak(this, WeakCallback, v8::WeakCallbackType::kParameter);
}
複製程式碼

JSC 引擎管理物件 GC 行為的介面

JSC 引擎上常用於和 JS 打交道的物件包括 JSContextGroupRef / JSContextRef / JSValueRef / JSObjectRef / JSStringRef / JSClassRef 等,對於這些物件的記憶體管理的方式和 V8 引擎上的方式有所不同。在 JSC 引擎上直接接觸到的分為包裝和指標,而指標部分由引擎 GC 管理,需要開發者手動管理。

JSStringRef、 JSContextRef、JSContextGroupRef、JSClassRef 就是典型的不受管理的指標,當開發者建立了它們之後,必須在不需要使用的時候釋放它們的記憶體,否則會出現記憶體洩露。請看下面的示例。

JSContextGroupRef context_group = JSContextGroupCreate();
JSClassRef global_class = JSClassCreate(&kJSClassDefinitionEmpty);
JSContextRef context = JSGlobalContextCreateInGroup(context_group, global_class);
JSStringRef str = JSStringCreateWithUTF8CString("try");
// 手動回收記憶體
JSClassRelease(golbal_class);
JSStringRelease(str);
JSGlobalContextRelease(context);
JSContextGroupRelease(context_group);
複製程式碼

JSValueRef 代表著 JS 物件( JSObjectRef 也是一個 JSValueRef ) 是由 JSC 引擎中的 GC 機制管理的,當 JSValueRef 建立出來後,當需要超出當前函式塊在全域性區域或者堆上繼續操作,則需要通過 JSValueProtect 對 JSValueRef 進行持久化的操作,JS 物件將脫離 GC 的管理。JSValueUnprotect 是 JSC 引擎提供的對於 JSValueRef 去持久化的 API。持久化和去持久化必須成對出現,否則會出現記憶體洩露。

// 假設已經初始化上下文
JSContextRef context; 
JSObjectRef object = JSObjectMake(context, NULL, NULL);
// 持久化操作
JSValueProtect(context, object);
// 去持久化操作
JSValueUnprotect(context, object);
複製程式碼

JSC 中沒有弱持久化的概念,通過類定義建立出來的物件都可以監聽其被 GC 的事件,這一點和 V8 不同。對於普通或是去持久化的 JS 物件, 監聽其解構函式的方式是在建立 JS 物件的類定義時候需要加入解構函式的鉤子。

static void FinalizeCallback(JSObjectRef object) {
    // do sth
}

void CreateObject(JSContextRef context) {
    JSClassDefinition definition = kJSClassDefinitionEmpty;
	definition.finalize = FinalizeCallback;
    JSClassRef class_ref = JSClassMake(definition);
    JSObjectRef object = JSObjectMake(context, class_ref, NULL);
    JSClassRelease(class_ref);
}
複製程式碼

瞭解了基本的 JS 引擎初始化、 Extension 和記憶體管理知識,對於後續 Lynx 上各個 JS Binding 的作用以及框架設計會有更好的理解。

Lynx 中 JS Binding 前期版本

Lynx 是一個由前端驅動的框架,需要建立本地物件和 JS 物件的關係,本地物件跟隨 JS 物件的生命週期。

Lynx 早期的快速實現首先在 Android 平臺上進行,依賴 V8 引擎,利用 Java 搭建了核心鏈路。由於 Android 並沒有直接和 V8 溝通的介面,通過引入 V8 的動態 so,Lynx 結合 JNI 實現 JS Binding,建立和 V8 的橋樑。整體設計如下圖。JS Binding 是作為 Android 和 JS 的通訊使者,JS Binding 包括了 JS 引擎初始化模組 JSContext 和一系列擴充模組,如 JS 上下文中 document、element 物件等。

Lynx技術分析-JS引擎擴充套件技術基礎

在前期版本中只有簡單的設計,主要目的是滿足功能。這裡主要介紹基本類及其作用和整體流程。

基礎類介紹

關鍵基礎類 UML 圖

Lynx技術分析-JS引擎擴充套件技術基礎

JSContext 用於初始化 V8 環境,向 V8 中註冊基礎物件,搭建中間層 JS Binding。通過 JS Binding 可以實現 Android 層面的物件和 V8 的間接呼叫。JS Binding 中包括 DocumentObject、WindowObject、ElementObject 等。

ElementObject 屬於 JS Binding 中的成員,作為兩端 Element 通訊的使者。其父類是 JavaObjectWrap。用於向 V8 中擴充 element 物件及註冊相關函式和變數鉤子,在 JS 上下文提供基礎 API。同時初始化 JNI 函式,準備和 Java Element 通訊基礎。與 ElementObject 作用類似的還有 DocumentObject、WindowObject 等。

JavaObjectWrap 作為通訊使者的基類,通過 V8 引擎和 JNI 提供的持久化介面是建立 Android 端物件和 V8 端物件的關係,使 Android 物件、c++物件和 V8 物件具有一致的生命週期。在 JavaObjectWrap 中持有 Java 持久化物件和 JS 持久化物件。繼承 Observer 類。

Observer 作用是確保頁面退出時,在 JSBinding 上沒有洩露的記憶體。在頁面退出時,防止在頁面退出後因為 JS 引擎的記憶體管理影響,導致 c++ 和 Android 的記憶體洩露,清理在 JS Binding 中的未被 GC 的物件。

整體流程

JS Binding 擔任了兩個不同平臺間交流的使者,是整個 Lynx 框架的基石。JS Binding 整體流程主要包括 JS Binding 初始化流程、JS 呼叫擴充 API 流程和 JS Binding 結束流程。

在頁面啟動時,JS Binding 會進行初始化操作,JS 上下文中就能具備了使用 Lynx 框架能力的功能,初始化流程如下

  1. Android 層通過 JNI 呼叫 JSContext::Initialize 方法進行引擎環境和上下文初始化。
  2. 呼叫 ElementObject、ConsoleObject 等 c++ 物件的 BindingClass 方法初始化與 JNI 相關的變數,如 jclass 和 jMethodID,以便後續呼叫 Java 方法;建立對應類的構造器函式模板,向函式模板中註冊鉤子。
  3. 建立 DocumentObject、ConsoleObject 的 c++ 物件,通過 JNI 方法 NewGlobalRef 持久化對應的 Java jobject 物件,並將自身的地址交給 Java 物件持有,建立 c++ 物件和 Java 物件的關係。
  4. 通過 DocumentObject、ConsoleObject 的構造器函式模板建立 JS 物件,如 document、console 等,關聯 c++ 物件到 JS 物件中。c++ 物件將 JS 物件持久化,註冊析構監聽。建立 c++ 物件和 JS 物件的關係。
  5. 將擴充的 JS 物件註冊到 JS 上下文中。

當 JS 中通過 document.createElement('view') 方法時,會簡潔呼叫到 Java 層介面並返回具體結果,具體流程如下

  1. V8 引擎在執行到對應方法時,回撥 DocumentObject 的 CreateElementCallback 靜態方法鉤子。
  2. 在 CreateElementCallback 方法中,獲取 JS 引擎物件中繫結的本地物件 DocumentObject,呼叫其 CreateElement 方法。
  3. 在 DocumentObject::CreateElement 方法中,通過 JNI 介面和已經初始化的 jMethod 和呼叫 Java 物件的 createElement 方法。
  4. 在 Java Document.createElement 方法中,建立 Element 物件並返回其持有的 c++ 指標 ElementObject 地址。
  5. 在 DocumentObject::CreateElement 方法中,強轉獲得 Java Element 物件持有的 ElementObject 指正地址並返回。
  6. 在 CreateElementCallback 方法中,返回 ElementObject 中持有的持久化 JS 物件。
  7. JS 中獲得 document.createElement('view') 呼叫結果 element。

Lynx 頁面退出代表著 JS Binding 的結束,Android 層通過 JNI 釋放 JSContext 記憶體,進而釋放 JS 物件、c++ 物件和 Java 物件所佔用的記憶體。觸發解構函式將完成以下操作

  1. 釋放所有持有的構造器函式模板,觸發 V8 引擎 GC。
  2. JS 物件 GC 觸發函式鉤子 JavaObjectWrap::WeakCallback,釋放持久化 jobject 物件,釋放本身 c++ 指標。

總結

這篇文章主要介紹 JS Binding 入門知識和 Lynx 框架在早期版本 JS Binding 的簡單實現,為後續的 Lynx 框架的 JS Binding 的演進拋磚引玉。下一篇文章將分析 Lynx 框架中 JS Binding 演進的技術,並介紹其優缺點。

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

附錄

example_v8.cc 初始化流程具體程式碼

// example.testStatic() 呼叫時對應的 c++ 函式鉤子
static void TestStaticCallback(const v8::FunctionCallbackInfo <v8::Value> &args) {
    Example* example = static_cast<Example*>(args.Holder()->GetAlignedPointerFromInternalField(0));
    example->TestStatic();
}

// example.testDynamic() 呼叫時對應的 c++ 函式鉤子
static void TestDynamicCallback(const v8::FunctionCallbackInfo <v8::Value> &args) {
    Example* example = static_cast<Example*>(args.Holder()->GetAlignedPointerFromInternalField(0));
    example->TestDynamic();
}

// console.log(example.num) 呼叫時對應觸發的 c++ 鉤子函式
static void GetNumCallback(v8::Local<v8::String> property, const PropertyCallbackInfo<Value>& info) {
    Example* example = static_cast<Example*>(args.Holder()->GetAlignedPointerFromInternalField(0));
    int num = example->num();
    info.GetReturnValue().Set(v8::Number::New(isolate, num));
}

// example.num = 99 時會觸發該的 c++ 函式鉤子
static void SetNumCallback(v8::Local<v8::String> property, v8::Local<v8::Value> value, const PropertyCallbackInfo<void>& info) {
    if (value->IsInt32()) {
        Example* example = static_cast<Example*>(args.Holder()->GetAlignedPointerFromInternalField(0));
    	example->set_num(value->ToInt32())
    }
}

void Example::Initialize() {
    // 初始化 V8 引擎
    V8::InitializeICUDefaultLocation(argv[0]);
    V8::InitializeExternalStartupData(argv[0]);
    v8::Platform* platform = v8::platform::CreateDefaultPlatform();
    V8::InitializePlatform(platform);
    v8::V8::Initialize();
    v8::Isolate* isolate = v8::Isolate::New(create_params);
    v8::Isolate::Scope isolate_scope(isolate);
    v8::HandleScope handle_scope(isolate);
    
	// 建立全域性 global 物件模板,根據模板建立 JS 執行上下文,在上下文 context 中獲取 global 物件
    v8::Local <v8::ObjectTemplate> global_template = v8::ObjectTemplate::New(isolate);
    v8::Local <v8::Context> context = v8::Context::New(isolate, nullptr, global_template);
    v8::Context::Scope context_scope(context);
    v8::Local<v8::Object> global = context->Global();
    
    // 建立 example 的建構函式模板, 使用該 c++ 類的初始化函式作為引數(函式鉤子),初始化構造器函式模
    // 板。即當呼叫建構函式建立物件時,會呼叫該鉤子函式做構造處理
    v8::Local<v8::FunctionTemplate> example_tpl = v8::FunctionTemplate::New(isolate);
    // 設定建構函式模板的類名
    example_tpl->SetClassName(V8Helper::ConvertToV8String(isolate, "Example"));
    // 設定內部關聯 c++ 物件的數量為 1
    example_tpl->InstanceTemplate()->SetInternalFieldCount(1);
    // 設定建構函式模板中的原型模板的對應函式名的鉤子
    example_tpl->PrototypeTemplate()->Set(V8Helper::ConvertToV8String(isolate, "testStatic"),
        v8::FunctionTemplate::New(isolate, TestStaticCallback));
    // 設定建構函式模板中的原型模板的屬性的 Get 和 Set 鉤子方法
    example_tpl->PrototypeTemplate()->SetAccessor(V8Helper::ConvertToV8String(isolate, "num"), GetNumCallback, SetNumCallback);
    // 在函式模板中獲取可呼叫的函式
    v8::Local<v8::Function> example_constructor = example_tpl->GetFunction(context).ToLocalChecked();
    // 呼叫函式的建立物件的方法,建立 JS 引擎的 example 物件
    v8::Local<v8::Object> example =
        example_constructor->NewInstance(context, 0, nullptr).ToLocalChecked();
    // 關聯 JS 引擎物件和 c++ 物件
    handle->SetAlignedPointerInInternalField(0, this);
    // 動態註冊函式鉤子
    v8::Local<v8::Function> dynamic_test_func = v8::FunctionTemplate::New(TestDynamicCallback, args.Data())->GetFunction();
    v8::Local<v8::String> dynamic_test_name = v8::String::NewFromUtf8(isolate, "testDynamic", v8::NewStringType::kNormal).ToLocalChecked();
    dynamic_test_func->SetName(dynamic_test_name);
    example->Set(dynamic_test_name, dynamic_test_func);
    // 向 global 物件中設定 example 屬性
    v8::Local<v8::String> example_v8_str = v8::String::NewFromUtf8(isolate, "example", v8::NewStringType::kNormal).ToLocalChecked()
    global->Set(context, example_v8_str, example).FromJust();
}
複製程式碼

初始化流程具體程式碼

// example.test() 呼叫時對應的 c++ 函式鉤子
static JSValueRef TestStaticCallback(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) {
    // 獲取 JS 引擎物件中持有的 c++ 物件
    Example* example = static_cast<Example*>(JSObjectGetPrivate(object));
    example->TestStatic();
}

// example.testDynamic() 呼叫時對應的 c++ 函式鉤子
static JSValueRef TestDynamicCallback(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) {
    Example* example = static_cast<Example*>(JSObjectGetPrivate(object));
    example->TestDynamic();
}

// console.log(example.num) 呼叫時對應觸發的 c++ 鉤子函式
static JSValueRef GetNumCallback(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef* exception) {
    Example* example = static_cast<Example*>(JSObjectGetPrivate(object));
    int num = obj->num();
    return JSValueMakeNumber(ctx, num);
}

// example.num = 99 時會觸發該的 c++ 函式鉤子
static bool SetNumCallback(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef value, JSValueRef* exception) {
    Example* example = static_cast<Example*>(JSObjectGetPrivate(object));
    obj->set_num(JSValueToNumber(ctx, value, NULL));
}

// 定義將要 Extension 的靜態方法,其中包含函式鉤子
static JSStaticFunction s_examplle_function_[] = {
    {"testStatic", TestStaticCallback, kJSClassAttributeNone},
    { 0, 0, 0 }
};

// 定義將要 Extension 的變數,其中包含 get 和 set 函式鉤子
static JSStaticValue s_example_values_[] = {
    {"num", GetNumCallback, SetNumCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
    { 0, 0, 0, 0 }
};

void Example::Initialize(JSVM* vm, Runtime* runtime) {
    // 初始化 JSC 引擎
    JSContextGroupRef context_group = JSContextGroupCreate();
    
	// 建立全域性 global 類的定義
    JSClassDefinition global_class_definition = kJSClassDefinitionEmpty;
    // 建立 global 物件的類
    JSClassRef global_class = JSClassCreate(&global_definition);
    // 根據 global 類建立上下文,從上下文獲取 global 物件
    JSContextRef context = JSGlobalContextCreateInGroup(context_group, global_class);
    JSObjectRef global = JSContextGetGlobalObject(context);
    
    // 建立 example 類的定義
    JSClassDefinition example_class_definition = kJSClassDefinitionEmpty;
    // 設定類的對應函式名和引數名的鉤子
    example_class_definition.staticFunctions = s_console_function_;
    example_class_definition.staticValues = s_console_values_;
    // 設定類的名稱
    example_class_definition.className = "Example";
    // 建立 JS 引擎的類
    JSClassRef example_class_ref = JSClassCreate(&example_class_definition);
    JSObjectRef example = JSObjectMake(context, example_class_ref, NULL);
    // 關聯 c++ 物件和 JS 引擎物件
    JSObjectSetPrivate(context, example, this)
    JSClassRelease(example_class_ref);
    // 動態註冊函式鉤子
    JSStringRef dynamic_test_func_name = JSStringCreateWithUTF8CString("testDynamic");
    JSObjectRef dynamic_test_func = JSObjectMakeFunctionWithCallback(context, dynamic_test_func_name, TestDynamicCallback);
    JSObjectSetProperty(context, example, dynamic_test_func_name, dynamic_test_func, kJSPropertyAttributeDontDelete, NULL);
    JSStringRelease(dynamic_test_func_name);
    // 向 global 物件中設定 example 屬性
    JSStringRef example_str_ref = JSStringCreateWithUTF8CString("example");
    JSObjectSetProperty(context, global, example_str_ref, example, kJSPropertyAttributeDontDelete, NULL);
    JSStringRelease(example_str_ref);
}
複製程式碼

相關文章