JsCore 原理和实践

2,680 阅读10分钟
原文链接: blog.csdn.net

前言

WebKit 是一个开源的浏览器引擎,与之相对应的引擎有Gecko(Mozilla Firefox 等使用)和Trident(也称MSHTML,IE 使用)。 同时WebKit 也是苹果Mac OS X 系统引擎框架版本的名称,主要用于Safari,Dashboard,Mail 和其他一些Mac OS X 程序。WebKit 前身是 KDE 小组的 KHTML,WebKit 所包含的 WebCore 排版引擎和 JSCore 引擎来自于 KDE 的 KHTML 和 KJS,当年苹果比较了 Gecko 和 KHTML 后,仍然选择了后者,就因为它拥有清晰的源码结构、极快的渲染速度。Apple将 KHTML 发扬光大,推出了装备 KHTML 改进型 WebKit 引擎的浏览器 Safari。

业界的浏览器都用 大部分用 WebKit 内核做自己的引擎。

一 WebKit引擎

使用WebKit的主要两个浏览器Sfari和Chromium(Chorme的开源项目)。WebKit起源于KDE的开源项目Konqueror的分支,由苹果公司用于Sfari浏览器。其一条分支发展成为Chorme的内核,2013年Google在此基础上开发了新的Blink内核。 webkit是sfari、chrome等浏览器的排版引擎,各部分架构图如下

webkit

WebKit 所包含的 WebCore绘制引擎和 JSCore 引擎,均是从KDE的KHTML及KJS引擎衍生而来。它们都是自由软件,在GPL条约下授权,同时支持BSD系统的开发。所以Webkit也是自由软件,同时开放源代码。

WebCore 与平台无关,各个浏览器移植所共享。包含HTML解析,CSS解析,渲染,调试等部分。

WebKit就是一个页面渲染以及逻辑处理引擎,前端工程师把HTML、JavaScript、CSS这“三驾马车”作为输入,经过WebKit的处理,就输出成了我们能看到以及操作的Web页面。从上图我们可以看出来,WebKit由图中框住的四个部分组成。而其中最主要的就是WebCore和JSCore(或者是其它JS引擎),这两部分我们会分成两个小章节详细讲述。除此之外,WebKit Embedding API是负责浏览器UI与WebKit进行交互的部分,而WebKit Ports则是让Webkit更加方便的移植到各个操作系统、平台上,提供的一些调用Native Library的接口,比如在渲染层面,在iOS系统中,Safari是交给CoreGraphics处理,而在Android系统中,Webkit则是交给Skia。

1.1WebCore

在上面的WebKit组成图中,我们可以发现只有WebCore是红色的。这是因为时至今日,WebKit已经有很多的分支以及各大厂家也进行了很多优化改造,唯独WebCore这个部分是所有WebKit共享的。WebCore是WebKit中代码最多的部分,也是整个WebKit中最核心的渲染引擎。那首先我们来看看整个WebKit的渲染流程:

webcore

首先浏览器通过URL加载一堆由HTML、CSS、JS组成的资源文件,通过加载器(这个加载器的实现也很复杂,在此不多赘述)把资源文件给WebCore。之后HTML Parser 会把HTML解析成DOM树,CSS Parser会把CSS解析成CSSOM树。最后把这两棵Tree 合并,生成最终需要的渲染树,再经过布局,与具体WebKit Ports的渲染接口,把渲染树渲染输出到屏幕上,成为了最终呈现在用户面前的网页。

简单总结:

主要从网页的 URL 到构建完 DOM 树,接着 从 DOM 树到构建完 WebKit 的绘图上下文,从绘图上下文到生成最终的UI图像。

1.2 JavascriptCore

业界流行的动态化方案,如Facebook的RN,阿里的Weex 都采用了前端系的DSL方案,而它们在iOS系统上能够顺利的运行,都离不开一个背后的功臣:JavaScriptCore(以下简称JSCore),它建立起了OC和JS 两门语言之间沟通的桥梁。无论是这些流行的动态化方案,还是WebView Hybrid方案,亦或是之前广泛流行的JSPatch,JSCore都在其中发挥了举足轻重的作用。作为一名Android 开发工程师,如果想从事跨平台开发和类似RN框架实现,了解 JSCore已经逐渐成为了必备技能之一。

JavascriptCore是使用在ReactNative和iOS平台上的Javascript引擎。目前 JavaScript 引擎还有 Google 的 V8 ,Mozilla 的 SpiderMonkey。 当然Android是同个类的是JavascriptInterace.

JavaScriptCore是一个优化的VM。 JavaScriptCore由以下构建块组成:词法分析器,解析器,启动解释器(LLInt),基线JIT,低延迟优化JIT(DFG)和高并发优化JIT(FTL)。

Lexer:词法分析器,生成 tokens,大部分代码都在 parser/Lexer.cpp 里。

Parser:语法分析,基于 Lexer 的 tokens 生成语法树。手写了个 recusive descent parser 递归下降解析器,代码主要在 parser/Parser.cpp 里。

LLInt:Low Level Interpreter 执行 Parser 生成的 Byte code。代码在 llint/ 里,使用汇编,在 offlineasm/ 里,可以编译为 x86 和 ARMv7 的汇编和 C 代码。LLInt 希望达成除了词法和语法分析外零启动消耗,同时遵守用 JIT 在调用,堆栈和起存器的约定。

Baseline JIT:实时编译,性能不好用这个。在函数调用了 6 次,或者某段代码循环了大于100次会被触发。BaseLine JIT 的代码在 jit/ 里。BaseLine JIT 还对几乎所有堆的访问执行了复杂的多态内联高速缓存(Polymorphic inline caches)。多态内联缓存是 Smalltalk 社区优化动态分发的一个经典技术。

DFG JIT:低延迟优化 JIT,更差性能就用这个生成更优化的机器码来执行。在函数被调用了60次或者代码循环了1000次会触发。在 LLInt 和 Baseline JIT 中会收集一些包括最近参数,堆以及返回值中的数据等轻量级的性能信息,方便 DFG 进行类型判断。先获取类型信息可以减少大量的类型检查,推测失败 DFG 会取消优化,也叫 OSR exit。取消可以是同步的也可以是异步的。取消后会回到 Baseline JIT,退回一定次数会进行重新优化,收集更多统计信息,看情况再次调用 DFG。重新优化使用的是指数式回退策略应对一些怪异的代码。DFG 代码在 dfg/ 里。
FTL:高吞吐量优化 JIT,全称 Faster Than Light,DFG 高层优化配合 B3 底层优化。以前全称是 Fourth Tier LLVM 底层优化使用的是 LLVM。B3 对 LLVM 做了裁剪,对 JavaScriptCore 做了特性处理,B3 IR 的接口和 LLVM IR 很类似。B3 对 LLVM 的替换主要是考虑减少内存开销,LLVM 主要是针对编译器,编译器在这方面优化动力必然没有 JIT 需求高。B3 IR 将指针改成了更紧凑的整数来表示引用关系。不可变的常用的信息使用固定大小的结构来表示,其它的都放到另外的地方。紧凑的数据放到数组中,更多的数组更少的链表。这样形成的 IR 更省内存。Filip Pizlo 主导的这个改动,DFG JIT 也是他弄的,为了能够更多的减少内存上的开销,他利用在 DFG 里已经做的 InsertionSet 将 LLVM IR 里的 def-use 干掉了,大概思路是把单向链表里批量插入新 IR 节点先放到 InsertionSet 里,在下次遍历 IR 时再批量插入。Filip Pizlo 还把 DFG 里的 UpsilonValue 替代 LLVM SSA 组成部分。B3 后面会把 LLVM 的寄存器分配算法 Greedy 一直到 B3 中。

再来看看JsCore的核心类

JSContext 代表JS的执行环境,通过-evaluateScript:方法就可以执行JS代码

JSValue 封装了JS与OC中的对应的类型,以及调用JS的API等

JSExport 是一个协议,遵守此协议,就可以定义我们自己的协议,在协议中声明的API都会在JS中暴露出来,才能调用

JsCore

在 Native 中开启多个线程来异步执行不同API ,也就意味着开发者可创建多个 JSVirtualMachine VM,同时相互隔离不影响,这样保证了并行地执行不同 JS 任务。

在一个 JSVirtualMachine 中还可以关联多个 JSContext,并通过 JSValue( 来和 Native 进行数据传递通信,同时可以通过 JSExport,将 Native 中遵守此解析的类的方法和属性转换为 JS 的接口供其调用。

1.1.2 JsContext

创建的JSContextGroup,并提供Js执行的上下文环境。主要接口有如下:

/*!
*创建一个  JSContextGroup.
*/
JS_EXPORT JSContextGroupRef JSContextGroupCreate() CF_AVAILABLE(10_6, 7_0);

/*!
* 在提供的上下文组中创建新的全局JavaScript执行上下文。
*/
JS_EXPORT JSContextGroupRef JSContextGroupRetain(JSContextGroupRef group) CF_AVAILABLE(10_6, 7_0);

/*!
释放 JSContextGroup资源
*/
JS_EXPORT void JSContextGroupRelease(JSContextGroupRef group) CF_AVAILABLE(10_6, 7_0);

/*!
释放全局JavaScript执行上下文
*/
JS_EXPORT JSGlobalContextRef JSGlobalContextCreate(JSClassRef globalObjectClass) CF_AVAILABLE(10_5, 7_0);

/*!
* 保留全局JavaScript执行上下文
*/
JS_EXPORT JSGlobalContextRef JSGlobalContextCreateInGroup(JSContextGroupRef group, JSClassRef globalObjectClass) CF_AVAILABLE(10_6, 7_0);

/*!
* 保留全局JavaScript执行上下文
*/
JS_EXPORT JSGlobalContextRef JSGlobalContextRetain(JSGlobalContextRef ctx);

/*!
* 释放全局JavaScript执行上下文
*/
JS_EXPORT void JSGlobalContextRelease(JSGlobalContextRef ctx);

/*!
* 获取JavaScript执行上下文的全局对象
*/
JS_EXPORT JSObjectRef JSContextGetGlobalObject(JSContextRef ctx);

/*!
* 获取JavaScript执行上下文所属的上下文Group
*/
JS_EXPORT JSContextGroupRef JSContextGetGroup(JSContextRef ctx) CF_AVAILABLE(10_6, 7_0);

/*!
* 获取JavaScript执行上下文的全局上下文
*/
JS_EXPORT JSGlobalContextRef JSContextGetGlobalContext(JSContextRef ctx) CF_AVAILABLE(10_7, 7_0);

/*!
*获取上下文名称的副本
*/
JS_EXPORT JSStringRef JSGlobalContextCopyName(JSGlobalContextRef ctx) CF_AVAILABLE(10_10, 8_0);

/*!
* 设置上下文的远程调试名
*/
JS_EXPORT void JSGlobalContextSetName(JSGlobalContextRef ctx, JSStringRef name) CF_AVAILABLE(10_10, 8_0);

#ifdef __cplusplus
}
#endif

#endif /* JSContextRef_h */

1.1.3 JsBase

JavaScript引擎接口 ,主要负责 JavaScript 执行上下文,保存全局对象和其他执行状态。提供定义 JavaScript数据类型。

*/
JS_EXPORT bool JSCheckScriptSyntax(JSContextRef ctx, JSStringRef script, JSStringRef sourceURL, int startingLineNumber, JSValueRef* exception);

/*!
在JavaScript执行期间,您不需要调用此函数; 该
JavaScript引擎将根据需要进行垃圾收集。创建了JavaScript值
 在最后一个引用时,上下文组中的内容会自动销毁
*/
JS_EXPORT void JSGarbageCollect(JSContextRef ctx);

#ifdef __cplusplus
}
#endif

/* 运行时的平台启用Objective-C API */
#if !defined(JSC_OBJC_API_ENABLED)
#ifndef JSC_OBJC_API_AVAILABLE_MAC_OS_X_1080
#define JSC_OBJC_API_ENABLED (defined(__clang__) && defined(__APPLE__) && ((defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 1090 && !defined(__i386__)) || (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE)))
#else
#define JSC_OBJC_API_ENABLED (defined(__clang__) && defined(__APPLE__) && ((defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 1080 && !defined(__i386__)) || (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE)))
#endif
#endif

#endif /* JSBase_h */

好了知道JsCore核心后,我们看看怎么使用。

1.2 JsCore 实践

总体来说,JavaScriptCore的API是非常简单的,主要操作步骤如下:

1.2.1.get一个JSContext。

_jsContext = [_webView valueForKeyPath: @"documentView.webView.mainFrame.javaScriptContext"];

1.2.2. 处理解释某个JS 调起的方法,比如log

  _jsContext[@"log"] = ^() { NSArray *args = [JSContext currentArguments]; for (id obj in args) { 
        //需要注意这里的obj都还是
        JSValue NSLog(@"%@",obj);
         } };

1.2.3.调用JS端执行某个JS方法

[_jsContext evaluateScript:@"log('arg1')"]; [_jsContext evaluateScript:@"logCallback('arg1')"];

1.2.4 重定义某个JS端的方法

[_jsContext evaluateScript:@"checkAPI = function(){\ return [\ 'selectImage',\ 'startRecord',\ 'login',\ ];\ }"];

ios 详细jsCore请移步:trac.webkit.org/wiki/JavaSc…

1.3 iOS与JS 交互的解决方案

1.3 .1 OC 里面执行 JS

比如我们算算我的年纪

JSContext *jsContext = [[JSContext alloc] init];
  [jsContext evaluateScript:@"var name = tamic"];
  [jsContext evaluateScript:@"var computeAge = function(value)
                             { if (name == tamic) return  29 }"];
  JSValue *value = [jsContext evaluateScript:@"computeAge(num)"];
  int  age = [value  toInt32];

1.3 .2 JS调用OC

//Block block其实就等于android 的接口回调,可以将代码块做参数传进去,以”getTamicName"为名传递给JavaScript上下文

  JSContext *jsContext = [webView valueForKeyPath:
            @"documentView.webView.mainFrame.javaScriptContext"];
   jsContext[@"getTamicName"] = ^() {
       NSArray *args = [JSContext currentArguments];
       for (JSValue *obj in args) {
           NSLog(@"%@", obj);
       }
    };

上面是比较基础的案列。

目前 比较著名的实现有基于jsCore的 JavascriptBridge。
github.com/marcuswesti…

1.4 总结

到此我们学习Jscore的原理和js和OC的通讯, 下篇,我们接着讲Android中 的 JavascriptInterace。

更多我的技术文章 关注开发者技术前线公众号。

开发者技术前线