屏幕前的朋友你好,我是老陈。
今天这篇文章是原生与前端共舞模块的第一篇,跟你聊聊前端和原生之间的桥梁 JavaScriptCore,看看它是什么、能做哪些事儿。
总结来说,JavaScriptCore 为原生编程语言 Objective-C、Swift 提供调用 JavaScript 程序的动态能力,还能为 JavaScript 提供原生能力来弥补前端所缺能力。
正是因为JavaScriptCore的这种桥梁作用,所以出现了很多使用 JavaScriptCore 开发 App 的框架 ,比如React Native、Weex、小程序、WebView Hybird等框架。
接下来,我们再回过头来看看,JavaScriptCore 的来头是啥,为什么这些框架不约而同地都要用 JavaScriptCore 引擎来作为前端和原生的桥梁呢?
要回答这个问题的话,你还需要了解JavaScriptCore 的背景。
JavaScriptCore,原本是 WebKit中用来解释执行 JavaScript 代码的核心引擎。解释执行 JavaScript 代码的引擎自 JavaScript 诞生起就有,不断演进,一直发展到现在,如今苹果公司有 JavaScriptCore 引擎、谷歌有 V8 引擎、Mozilla 有 SpiderMonkey。对于 iOS 开发者来说,你只要深入理解苹果公司的 JavaScriptCore 框架就可以了。
iOS7 之前,苹果公司没有开放 JavaScriptCore 引擎。如果你想使用 JavaScriptCore 的话,需要手动地从开源WebKit 中编译出来,其接口都是 C 语言,这对于iOS开发者来说非常不友好。
但是从iOS7开始,苹果公司开始将 JavaScriptCore 框架引入 iOS 系统,并将其作为系统级的框架提供给开发者使用。这时,接口使用 Objective-C 进行包装,这对于原生 Objective-C 开发者来说,体验上就非常友好了。
JavaScriptCore 框架的框架名是 JavaScriptCore.framework。由于苹果公司的系统已经内置了JavaScriptCore 框架,而且性能不逊色于 V8 和 SpiderMonkey 等其他引擎,所以前端开发 App 框架就都不约而同将 JavaScriptCore 框架作为自己和原生的桥梁。
接下来,我就和你详细分析一下JavaScriptCore框架的原理。
苹果官方对JavaScriptCore框架的说明,你可以点击这个链接查看。从结构上看,JavaScriptCore 框架主要由 JSVirtualMachine 、JSContext、JSValue类组成。
JSVirturalMachine的作用,是为 JavaScript 代码的运行提供一个虚拟机环境。在同一时间内,JSVirtualMachine只能执行一个线程。如果想要多个线程执行任务,你可以创建多个 JSVirtualMachine。每个 JSVirtualMachine 都有自己的 GC(Garbage Collector,垃圾回收器),以便进行内存管理,所以多个 JSVirtualMachine 之间的对象无法传递。
JSContext 是 JavaScript 运行环境的上下文,负责原生和 JavaScript 的数据传递。
JSValue 是 JavaScript 的值对象,用来记录 JavaScript 的原始值,并提供进行原生值对象转换的接口方法。
JSVirtualMachine、JSContext、JSValue 之间的关系,如下图所示:

可以看出,JSVirtualMachine 里包含了多个 JSContext, 同一个JSContext 中又可以有多个 JSValue。
JSVirtualMachine 、JSContext、JSValue 类提供的接口,能够让原生应用执行 JavaScript 代码,访问 JavaScript 变量,访问和执行 JavaScript 函数;也能够让 JavaScript 执行原生代码,使用原生输出的类。
那么,解释执行 JavaScript 代码的 JavaScriptCore 和原生应用是怎么交互的呢?
要理解这个问题,我们先来看看下面这张图:

可以看到,每个 JavaScriptCore 中的 JSVirtualMachine 对应着一个原生线程,同一个 JSVirtualMachine 中可以使用 JSValue 与原生线程通信,遵循的是JSExport协议:原生线程可以将类方法和属性提供给 JavaScriptCore 使用,JavaScriptCore 可以将JSValue提供给原生线程使用。
JavaScriptCore 和原生应用要想交互,首先要有 JSContext。JSContext 直接使用 init 初始化,会默认使用系统创建的 JSVirtualMachine。如果 JSContext 要自己指定使用哪个 JSVirtualMachine,可以使用 initWithVirtualMachine 方法来指定,代码如下:
// 创建 JSVirtualMachine 对象 jsvm
JSVirtualMachine *jsvm = [[JSVirtualMachine alloc] init];
// 使用 jsvm 的 JSContext 对象 ct
JSContext *ct = [[JSContext alloc] initWithVirtualMachine:jsvm];
如上面代码所示,首先初始化一个 JSVirtualMachine 对象 jsvm,再初始化一个使用 jsvm 的 JSContext 对象 ct。
下面我再举一个通过JavaScriptCore在原生代码中调用JavaScript变量的例子。
这里有一段 JavaScript 代码,我定义了一个 JavaScript 变量 i ,然后我们一起看看如何通过 JavaScriptCore 在原生中调用变量i。代码如下:
JSContext *context = [[JSContext alloc] init];
// 解析执行 JavaScript 脚本
[context evaluateScript:@"var i = 4 + 8"];
// 转换 i 变量为原生对象
NSNumber *number = [context[@"i"] toNumber];
NSLog(@"var i is %@, number is %@",context[@"i"], number);
上面代码中,JSContext 会调用 evaluateScript 方法,返回 JSValue 对象。
JSValue 类提供了一组将 JavaScript 对象值类型转成原生类型的接口,你可以点击这个链接,查看官方文档中对 JSValue 接口的详细说明。
其中,有3个转换类型的接口比较常用,我来和你稍作展开:
如果你想在原生代码中使用JavaScript 中的函数对象,可以通过 callWithArguments 方法传入参数,然后实现它的调用。使用示例如下:
// 解析执行 JavaScript 脚本
[context evaluateScript:@"function addition(x, y) { return x + y}"];
// 获得 addition 函数
JSValue *addition = context[@"addition"];
// 传入参数执行 addition 函数
JSValue *resultValue = [addition callWithArguments:@[@(4), @(8)]];
// 将 addition 函数执行的结果转成原生 NSNumber 来使用。
NSLog(@"function is %@; reslutValue is %@",addition, [resultValue toNumber]);
如上面代码所示:首先,JSContext 通过 evaluateScript 方法获取 JavaScript 代码中 addition 函数,并保存为一个 JSValue 对象;然后,通过 JSValue 的 callWithArguments 方法,传入 addition 函数所需参数 x、y 以执行函数。
而如果要在原生代码中调用JavaScript 全局函数,你需要使用 JSValue 的 invokeMethod:withArguments 方法。比如,Weex框架就是使用这个方法,来获取 JavaScript 函数的。
相关代码路径是 incubator-weex/ios/sdk/WeexSDK/Sources/Bridge/WXJSCoreBridge.mm ,核心代码如下:
- (JSValue *)callJSMethod:(NSString *)method args:(NSArray *)args {
WXLogDebug(@"Calling JS... method:%@, args:%@", method, args);
return [[_jsContext globalObject] invokeMethod:method withArguments:args];
}
可以看到,JSContext 中有一个 globalObject 属性。globalObject 是 JSValue 类型,里面记录了 JSContext 的全局对象,使用 globalObject 执行的 JavaScript 函数能够使用全局 JavaScript 对象。因此,通过 globalObject 执行 invokeMethod:withArguments 方法就能够去使用全局 JavaScript 对象了。
通过上面的分析,我们可以知道,通过 evaluateScript 方法,就能够在原生代码中执行 JavaScript 脚本,并使用 JavaScript 的值对象和函数对象。那么,JavaScript又是如何调用原生代码呢?
我先给出一段代码示例,你可以思考一下是如何实现的:
// 在 JSContext 中使用原生 Block 设置一个减法 subtraction 函数
context[@"subtraction"] = ^(int x, int y) {
return x - y;
};
// 在同一个 JSContext 里用 JavaScript 代码来调用原生 subtraction 函数
JSValue *subValue = [context evaluateScript:@"subtraction(4,8);"];
NSLog(@"substraction(4,8) is %@",[subValue toNumber]);
可以看出,JavaScript 调用原生代码的方式,就是:
● 首先,在 JSContext 中使用原生 Block 设置一个减法函数subtraction;
● 然后,在同一个 JSContext 里用 JavaScript 代码来调用原生 subtraction 函数。
除了 Block外,我们还可以通过 JSExport 协议来实现在JavaScript中调用原生代码,也就是原生代码中让遵循JSExport协议的类,能够供 JavaScript 使用。Weex 框架里,就有个遵循了 JSExport 协议的WXPolyfillSet 类,使得JavaScript 也能够使用原生代码中的 NSMutableSet 类型。
WXPolyfillSet 的头文件代码路径是 incubator-weex/ios/sdk/WeexSDK/Sources/Bridge/WXPolyfillSet.h ,内容如下:
@protocol WXPolyfillSetJSExports <JSExport>
// JavaScript 可以使用的方法
+ (instancetype)create;
- (BOOL)has:(id)value;
- (NSUInteger)size;
- (void)add:(id)value;
- (BOOL)delete:(id)value;
- (void)clear;
@end
// WXPolyfillSet 遵循 JSExport 协议
@interface WXPolyfillSet : NSObject <WXPolyfillSetJSExports>
@end
可以看到,WXPolyfillSet 通过 JSExport 协议,提供了一系列方法给 JavaScript 使用。
现在我们已经理解了原生和 JavaScript 的互通方式,知道了它们的互通依赖于虚拟机环境JSVirtualMachine。接下来,我们需要对JavaScriptCore引擎进行更深入地理解,才能更好地用好这个框架。比如,JavaScriptCore 是怎么通过直接使用缓存 JIT 编译的机器码来提高性能的,又是怎么对部分函数进行针对性测试编译优化的。
JSVirtualMachine 是一个抽象的 JavaScript 虚拟机,是提供给开发者进行开发的,而其核心的 JavaScriptCore 引擎则是一个真实的虚拟机,包含了虚拟机都有的解释器和运行时部分。其中,解释器主要用来将高级的脚本语言编译成字节码,运行时主要用来管理运行时的内存空间。当内存出现问题,需要调试内存问题时,你可以使用 JavaScriptCore 里的 Web Inspector,或者通过手动触发 Full GC 的方式来排查内存问题。
接下来,我跟你说下 JavaScriptCore 引擎内部的组成。
JavaScriptCore内部是由 Parser、Interpreter、Compiler、GC 等部分组成,其中 Compiler 负责把字节码翻译成机器码,并进行优化。你可以点击这个链接,来查看WebKit 官方对JavaScriptCore 引擎的介绍。
JavaScriptCore 解释执行 JavaScript 代码的流程,可以分为两步。
第一步,由 Parser 进行词法分析、语法分析,生成字节码。
第二步,由 Interpreter 进行解释执行,解释执行的过程是先由 LLInt(Low Level Interpreter)来执行 Parser 生成的字节码,JavaScriptCore 会对运行频次高的函数或者循环进行优化。优化器有 Baseline JIT、DFG JIT、FTL JIT。对于多优化层级切换, JavaScriptCore 使用 OSR(On Stack Replacement)来管理。
今天这篇文章,我主要和你分享的是 iOS 中 JavaScriptCore 能干的事情。
总结来说,JavaScriptCore 提供了前端与原生相互调用的接口,接口层上主要用的是 JSContext 和 JSValue 这两个类,通过 JSValue 的 evaluateScript 方法、Block 赋值 context、JSExport 协议导出来达到互通的效果。
前端的优势在于快速编写UI,原生的优势在于对平台特性的天然支持,现在我们有了能够打通前端和原生的武器,就可以充分利用二者的优势,互为补充地去做更多、更有意思的事儿。而你,也可以充分发挥自己的想象力,去实现更有意思的App。
如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby
在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has
这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下
我的主要目标是能够完全理解我正在使用的库/gem。我尝试在Github上从头到尾阅读源代码,但这真的很难。我认为更有趣、更温和的踏脚石就是在使用时阅读每个库/gem方法的源代码。例如,我想知道RubyonRails中的redirect_to方法是如何工作的:如何查找redirect_to方法的源代码?我知道在pry中我可以执行类似show-methodmethod的操作,但我如何才能对Rails框架中的方法执行此操作?您对我如何更好地理解Gem及其API有什么建议吗?仅仅阅读源代码似乎真的很难,尤其是对于框架。谢谢! 最佳答案 Ru
有没有办法配置(例如,可以使用Fastfile)或以更简洁的方式执行FaSTLane?它目前打印出很多信息,这些信息通常会使开发人员对警告和错误视而不见。主要问题是需要花费一些时间在大量无用消息中滚动和搜索黄色/红色文本,直到您了解发生了什么。默认设置会打印所有内容,令人惊讶的是甚至还有--verbosemode对于CLI,但我找不到任何相反的东西,例如--quiet模式。编辑:下面是一些我希望能够抑制的输出示例。考虑到我使用了来自gitrepo的Fastfile,gym、match、cocoapods、get_version_number、increment_version_numb
我的假设是moduleAmoduleBendend和moduleA::Bend是一样的。我能够从thisblog找到解决方案,thisSOthread和andthisSOthread.为什么以及什么时候应该更喜欢紧凑语法A::B而不是另一个,因为它显然有一个缺点?我有一种直觉,它可能与性能有关,因为在更多命名空间中查找常量需要更多计算。但是我无法通过对普通类进行基准测试来验证这一点。 最佳答案 这两种写作方法经常被混淆。首先要说的是,据我所知,没有可衡量的性能差异。(在下面的书面示例中不断查找)最明显的区别,可能也是最著名的,是你的
几个月前,我读了一篇关于rubygem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:
我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的
我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur
前言作为一名程序员,自己的本质工作就是做程序开发,那么程序开发的时候最直接的体现就是代码,检验一个程序员技术水平的一个核心环节就是开发时候的代码能力。众所周知,程序开发的水平提升是一个循序渐进的过程,每一位程序员都是从“菜鸟”变成“大神”的,所以程序员在程序开发过程中的代码能力也是根据平时开发中的业务实践来积累和提升的。提高代码能力核心要素程序员要想提高自身代码能力,尤其是新晋程序员的代码能力有很大的提升空间的时候,需要针对性的去提高自己的代码能力。提高代码能力其实有几个比较关键的点,只要把握住这些方面,就能很好的、快速的提高自己的一部分代码能力。1、多去阅读开源项目,如有机会可以亲自参与开源