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,一个高性能跨平台开发框架。

相关文章
相关标签/搜索