JavaScriptCore简介
iOS 7中加入了JavaScriptCore框架,该框架让Objective-C和JavaScript代码直接交互变得更加简单方便。JavaScriptCore是Cocoa API,用于建立JavaScript与 Swift or Objective- C的桥梁。该框架允许我们在基于Swift or Objective- C的程序中执行 JavaScript程序,也可以插入自定义对象到JavaScript环境中。 JavaScriptCore框架对外暴露的类非常少,只有5个类,分别是JSContext,JSValue,JSManagedValue,JSVirtualMachine,JSExport,其中最核心的是JSContext和JSValue。下面一起来看一下几个部分:
JSContext
JSContext对象就是JavaScript代码执行的环境,也可以理解为作用域(也就是所有的JavaScript执行操作都将在JSContext中发生)。你可以在 Objective-C或者Swift代码中创建和使用 JavaScript contexts来执行(evaluate) JavaScript scripts,访问JavaScript中定义的values,甚至直接在JavaScript中访问原生应用中的对象,方法,函数。一个 JSContext是一个全局环境的实例。
JSContext能够让我们创建新的变量和获取已经存在的变量。在单一的虚拟机(virtual machine)中我们能够拥有多个JSContext。在同一虚拟机(virtual machine)中的两个context可以进行信息交换,但是不能够在两个virtual machine中。如下图所示:
JSContext也用于管理JavaScript虚拟机中对象的生命周期,每一个JSValue实例都将与JSContext通过强引用相关联。只要JSValue存在,JSContext就会保持引用,当JSContext中所有的JSValue被释放掉,那么JSContext也将会被释放,除非之前有被retained。
JSValue
JSValue可以说是JavaScript和Object-C或Swift之间数据互换的桥梁。为了在原生代码(native code)和JavaScript代码之间传递数据,JSContext里的不同的Javascript值都可以封装在JSValue的对象里,包括字符串、数值、数组、函数等,甚至还有Error以及null和undefined;同时这个类型的对象可以方便快速地转化为swift里常用的数据类型,如toBool()、toInt32()、toArray()、toDictionary()等。我们也可以使用JSValue创建JavaScript对象来包装原生自定义类中的对象,或者通过原生的方法或block来提供JavaScript函数的实现。
每一个JSValue实例都是来自于JSContext,JSValue则包含了对context对象的强应用,这点需要特别注意,如果不注意可能会造成内存泄露。当我们通过JSValue调用方法时,返回的新JSValue跟之前的JSValue是属于同一个context的。
每一个JavaScript值也与具体的JSVirtualMachine对象相关联(间接通过context属性),该对象拥有这底层环境执行的资源。JSValue只能在同一个JSVirtualMachine之传递,如果传递到另外一个JSVirtualMachine,就会产生一个OC异常。之前的图片和相关内容也有提到过。
Converting Between JavaScript and Native Types 具体转换如下图:
JSExport
这是一个协议而不是对象。正如名字含义一样,我们可以使用这个协议暴露原生对象,实例方法,类方法,和属性给JavaScript,这样JavaScript就可以调用相关暴露的方法和属性。遵守JSExport协议,就可以定义我们自己的协议,在协议中声明的API都会在JS中暴露出来。
Exporting Objective-C Objects to JavaScript
当通过OC实例创建一个JavaScript Value的时候,并且JSValue类没有特别指明赋值协议(copying convention),JavaScriptCore会通过创建一个JavaScript Wrapper Object来wrap这个OC实例。(对于特定的类,JavaScriptCore会自动赋值values到适当的JavaScript类型,例如,NSString instances 变成 JavaScript strings)。
在JS中,继承是通过原型链来实现的。对于每个输出(export)的OC类,JavaScriptCore都会在对应的JavaScript context中创建一个原型(prototype)。对于NSObject类对应的prototype object是JavaScript Context的Object prototype。对于其他的OC类,JavaScriptCore会创建一个prototype,这个protocol的内部 [Prototype] 属性会指向JavaScriptCore为它的父类创建的那个protocol。通过这种方式,JavaScript wrapper object的原型链就能反应wrapper的类继承关系。
除了原型对象外,JavaScriptCore还为每一个OC类都创建了一个JavaScript constructor。
Exposing Objective-C Methods and Properties to JavaScript
默认情况下,OC类的所有方法和属性都不会被暴露给JavaScript,所以必须要选择要暴露的方法和属性。对于类遵守的任意协议(protocol),如果该协议包含了JSExport协议,则JavaScriptCore就会认为这个该协议中包含的方法和属性列表是暴露给JavaScript的。这个时候,我们才能在JavaScript调用OC类的exported的方法和属性。
对于每一个暴露的实例方法,JavaScriptCore创建了一个与之对应的函数作为原型对象的属性。对于暴露的OC属性,JavaScriptCore将在原型上创建一个JavaScript的存储属性。对于每一个暴露的类方法,JavaScriptCore将在构造对象上创建一个JavaScript函数。
具体的操作和使用,引用官方的例子:例1解释了采用 JSExport协议暴露属性和方法,例2是JavaScript中进行使用。
解释:OC中的属性声明确定与之对应的JavaScript属性:
如果OC中的属性被声明为readonly,JavaScript属性将有属性writable: false, enumerable: false, configurable: true.
如果OC中的属性被声明为readwrite,JavaScript属性将有属性writable: true, enumerable: true, configurable: true.
包裹OC属性,参数,并由JSValue使用自己的类型根据具体的复制协议(copying conventions)进行转换值。
注意:
如果一个类声明并遵守了JSExport协议,JavaScriptCore将忽略最初固有的copying conventions 。例如:如果我们自定义一个NSString的子类并遵守JSExport协议,而且传递一个类的实例到valueWithObject:方法,其结果是JavaScript包裹自定义类的对象,而不是最初的JavaScript string类型。
Customizing Export of Objective-C Selectors
当公开一个selector并拥有一个或者多个参数,JavaScriptCore将采用下列转换生成对应的函数名:
1)All colons are removed from the selector. selector中所有的冒号都将被去除。
2)Any lowercase letter that had followed a colon is capitalized.所有冒号之后的小写字母都将变为大写字母。
例如:转换Objective-C selector doFoo:withBar:到JavaScript,函数为doFooWithBar。
为了重命名selector暴露给JavaScript,,我们可以使用JSExportAs宏。比如:为了将OC中的selector doFoo:withBar:变成JavaScript函数doFoo,我们使用下列声明:
注意:JSExportAs仅仅适用于selector拥有一个或者多个参数。
JSVirtualMachine
一个JSVirtualMachine实例代表着JavaScript执行的自包含环境。使用这个类有两个目的:支持JavaScript并发执行、为JavaScript与Objective-C或Swift的过渡添加管理内存对象。
Threading and Concurrent JavaScript Execution
每一个JavaScript context(一个JSContext对象)都属于一个虚拟机(virtual machine).每一个虚拟机都能够包含多个contexts,并且允许在两个context之间传递values(一个JSValue对象)。然而,每一个虚拟机又是独特的,我们不能够传递一个value从一个虚拟机的context到另外一个虚拟机的context。
JavaScriptCore API是线程安全的,比如:我们能够在任意线程创建一个JSValue对象,或者执行script(evaluate scripts),所有其他的线程尝试使用相同的虚拟机都将进行等待。为了执行JavaScript多线程的并发,为每一个线程使用一个独立的 JSVirtualMachine。
Managing Memory for Exported Objects
当暴露Objective-C或者Swift object对象给 JavaScript,对象中一定不能够存储 JavaScript值,这种行为将创建循环引用-JSValue对象强引用JavaScript contexts,而JSContext对象强引用我们暴露给JavaScript的原生对象。为了解决这个问题,我们可以使用JSManagedValue类进行conditionally retain 一个Javascript值,并且暴露原生的持有者链给JavaScriptCore虚拟机来管理值。使用addManagedReference:withOwner:和removeManagedReference:withOwner:方法为JavaScriptCore描述你的原生对象图。在去除对象的最后一个管理引用之后,对象能够被JavaScript垃圾回收者安全释放。
JSManagedValue
JSValue的封装,用它可以解决JS和原生代码之间循环引用的问题。添加一个“conditional retain”行为来为值(values)提供自动内存管理。
注意:
不要在需要被暴露给JavaScript的原生对象中存储一个non-managed JSValue。因为JSValue对象将引用JSContext对象,这种行为将导致循环引用,阻止context被释放。
managed value的 “conditional retain”特性保证了只要在下面任意一个条件为true的情况下,managed value的underlying JavaScript value就会被retained.
The JavaScript value is reachable through the JavaScript object graph (that is, not subject to JavaScript garbage collection) .
The JSManagedValue object is reachable through the Objective-C or Swift object graph, as reported to the JavaScriptCore virtual machine using theaddManagedReference:withOwner: method.
如果上述条件都不成立,那JSManagedValue会被释放。
注意:
JSManagedValue对象非常相似于ARC中弱引用潜在的JSValue对象,如果并没有使用addManagedReference:withOwner:方法添加”“conditional retain”行为,当JavaScript的垃圾回收者释放了潜在的JavaScript值时,那么managed value的value属性将被自动置 nil。
下面一起来看一下基本使用:
1:在原生代码中执行JS代码
// MARK: 直接在原生代码中执行JS代码
func exampleOne(){
//当我们创建context的时候,它是空的。并没有变量和函数,所以需要创建上下文(context)中的变量和函数:
let context = JSContext() // 1 创建运行JavaScript代码的环境
context.evaluateScript("var number = 22") // 2 执行一段具体的JavaScript代码,往运行环境里加整形变量
context.evaluateScript("fruits = ['apple','banana','watermelon']")//添加数组
// 3 获取context中具体属性的值
let numberValue:JSValue = context.objectForKeyedSubscript("number")
print("numberValue is \(numberValue), \(numberValue)") // numberValue is 22, 22
context.setObject("Jack", forKeyedSubscript: "name") // 可以获取属性,同样也可以设置属性
let name = context.objectForKeyedSubscript("name")
print("name is \(name), \(name.toString())") // name is Jack, Jack
//我们之前创建的number变量在JavaScript code中是可以获取的
context.evaluateScript("var anotherNumber = number + 30")
let anotherNumber = context.objectForKeyedSubscript("anotherNumber")
print("anotherNumber is \(anotherNumber.toInt32()), \(anotherNumber)") //anotherNumber is 52, 52
let fruits = context.objectForKeyedSubscript("fruits")//取出JS运行环境里的数组,为JSValue类型
fruits.setObject("lemon", atIndexedSubscript: 5) //插入数据到数组
let firstFruits = fruits.objectAtIndexedSubscript(0)
print("firstFruits is \(firstFruits)") // firstFruits is apple
//MARK: 函数使用
// 注册js方法再利用JSValue调用
context.evaluateScript("var add = function(a,b) {return a + b}")
let addFunction = context.objectForKeyedSubscript("add") // 获取函数
let result = addFunction.callWithArguments([20,30]) // 调用函数,传入所需的参数
print("result is \(result.toInt32())") //result is 50
//或者直接调用
let secondResult = context.evaluateScript("add(20,30)")
print("secondResult is \(secondResult.toInt32())") //secondResult is 50
}
代码注释已经很清楚,从上面代码我们总结几点:
1、在OC/swift里,所有JavaScript代码都需要在JavaScript运行环境(JSContext)中通过evaluateScript运行;
2、在OC/swift里,所有JavaScript中的方法、对象、属性都需要通过objectForKeyedSubscript来取得,取得所有对象均为JSValue类型
3、通过objectForKeyedSubscript取得的JavaScript中的对象,都遵循该对象在JavaScript中有的所有特性,如上述代码中数组的长度,无数组越界,自动延展的特性
4、通过objectForKeyedSubscript取得的JavaScript中的方法,均可以通过callWithArguments传入参数调用JavaScript中的方法并返回正确的结果(类型仍然为JSValue)
2:执行本地文件或网络中的js代码
func exampleTwo(){
let path = NSBundle.mainBundle().pathForResource("testLocal", ofType: "js")
do{
let testScript = try String(contentsOfFile: path!, encoding: NSUTF8StringEncoding)
// 创建运行环境并指定具体的虚拟机,如果没有指定,系统会自动创建,这是初始化的另外一种方法。
let context = JSContext(virtualMachine: JSVirtualMachine())
context.evaluateScript(testScript)
// JacaScript环境中异常检测,一旦出现错误,闭包将会被执行
context.exceptionHandler = {context,exception in
print("JS Error:\(exception)")
}
let factorial = context.objectForKeyedSubscript("factorial")
let result = factorial.callWithArguments([10])
print("result is \(result.toInt32())") //result is 3628800
}catch let error as NSError{
print("\(error),\(error.userInfo)")
}
相关内容:
1:新建一个testLocal.js文件,如下图:点击创建文件,选择other->empty->输入testLocal.js
2:在testLocal.js文件中加入以下代码,计算阶乘
3:JavaScript中调用原生代码,可以通过两种方式:
1)Block方式:JS functions
2)JSExport protocol遵守协议:JS objects
block方式:
在js中运行原生代码,一种方法那就是block,他们将自动与JavaScript方法建立桥梁,然后,这里有一个小问题,需要注意:这种方法仅仅适用于OC的block。并不适用于swift中的闭包。为了公开闭包,我们将进行如下两步操作:
1)使用@convention(block) 属性标记闭包,来建立桥梁成为OC中的block。
2)在映射block到JavaScript方法调用之前,我们需要unsafeBitCast函数将block转成为AnyObject。
func exampleTwo(){
let path = NSBundle.mainBundle().pathForResource("testLocal", ofType: "js")
do{
let testScript = try String(contentsOfFile: path!, encoding: NSUTF8StringEncoding)
// 创建运行环境并指定具体的虚拟机,如果没有指定,系统会自动创建,这是初始化的另外一种方法。
let context = JSContext(virtualMachine: JSVirtualMachine())
context.evaluateScript(testScript)
// JacaScript环境中异常检测,一旦出现错误,闭包将会被执行
context.exceptionHandler = {context,exception in
print("JS Error:\(exception)")
context.exception = exception
}
let factorial = context.objectForKeyedSubscript("factorial")
let result = factorial.callWithArguments([10])
print("result is \(result.toInt32())") //result is 3628800
//context.evaluateScript("ider.zheng = 21");
}catch let error as NSError{
print("\(error),\(error.userInfo)")
}
}
使用Block的注意事项
从上面例子应该有体会到Block在JavaScriptCore中起到的强大作用,它在JavaScript和Objective-C之间的转换建立起更多的桥梁,让互通更方便。但是要注意的是无论是把Block传给JSContext对象让其变成JavaScript方法,还是把它赋给exceptionHandler属性,在Block内都不要直接使用其外部定义的JSContext对象或者JSValue,应该将其当做参数传入到Block中,或者通过JSContext的类方法+ (JSContext *)currentContext;来获得。否则会造成循环引用使得内存无法被正确释放。
比如上边自定义异常处理方法,就是赋给传入JSContext对象context,而不是其外创建的context对象,虽然它们其实是同一个对象。这是因为Block会对内部使用的在外部定义创建的对象做强引用,而JSContext也会对被赋予的Block做强引用,这样它们之间就形成了循环引用(Circular Reference)使得内存无法正常释放。
对于JSValue也不能直接从外部引用到Block中,因为每个JSValue上都有JSContext的引用,JSContext再引用Block同样也会形成引用循环。
JSExport protocol方式:
1)首先必须创建一个协议遵守JSExport协议,并声明想暴露在JavaScript中的属性和方法。
2)对于每一个暴露给JavaScript的原生类,JavaScriptCore都将在 JSContext中创建一个标准。默认情况下:类中是没有方法和属性暴露给JavaScript,所以,我们必须选择我们想要暴露的部分:下面是JSExport的部分规则:
1) For exported instance methods, JavaScriptCore creates a corresponding JavaScript function as a property of the prototype object.对于暴露的实例方法,JavaScriptCore将创建相对应的JavaScript函数作为一个原型对象的属性。
2) Properties of your class will be exported as accessor properties on the prototype. 类中是属性将作为原型中的访问器属性。
3)For class methods, the framework will create a JavaScript function on the constructor object.对于类方法, framework将在结构对象中创建 JavaScript函数。
首先,我们需要先定义一个协议,而且这个协议必须要遵守JSExport协议。注意,这里必须使用@objc,因为JavaScriptCore库是ObjectiveC版本的。如果不加@objc,则调用无效果。并创建一个自定义Item类,遵守自定义协议:
@objc protocol ItemExport: JSExport{
var name: String{get set}
func getNameDescription(message:String)
}
//创建一个Item,让它继承ItemExport协议
@objc class Item:NSObject,ItemExport{
dynamic var name: String
init(name:String) {
self.name = name
}
func getNameDescription(message: String) {
print("\(message) \(self.name)")
}
}
测试代码:
func exampleForJSExports(){
let context = JSContext(virtualMachine: JSVirtualMachine())
let item = Item(name: "Jack")
//非常重要,如果不建立连接,进行注入,那么是不可以进行交互。这一步是将item这个模型对象注入到JS中,在JS就可以通过item调用我们公暴露的方法了。key值可以任意设置。
context.setObject(item, forKeyedSubscript: "item")
//获取模型对象
let itemJSValue:JSValue = context.objectForKeyedSubscript("item")
let name = itemJSValue.objectForKeyedSubscript("name")
print("\(name)") // Jack
//调用item对象方法有如下几种:
//1:使用JSValue对象方法
itemJSValue .invokeMethod("getNameDescription", withArguments: ["My name is"]) //My name is Jack
//2:直接执行script
context.evaluateScript("item.getNameDescription('My name is')") //My name is Jack
}
4:JavaScript与原生控件的交互
//MARK:对系统类的操作
@objc protocol UIButtonJSExport:JSExport{
func setTitle(title: String,forState: UIControlState)
}
func exampleSystemControl(){
//注意:需要在oc和swift混编的桥梁文件中导入 #import <objc/runtime.h>,否则得不到运行时添加协议方法。
class_addProtocol(UIButton.self, UIButtonJSExport.self)
let button = UIButton(type: .Custom)
button.frame = CGRect(x: 60, y: 100, width: 200, height: 60)
button.setTitle("SwiftButton", forState: .Normal)
button.backgroundColor = UIColor.redColor()
button.addTarget(self, action: #selector(SwiftTwoViewController.clickButton), forControlEvents: .TouchUpInside)
view.addSubview(button)
buttonContext = JSContext()
buttonContext.setObject(button, forKeyedSubscript: "button")
buttonContext.exceptionHandler = {context,exception in
JSContext.currentContext().exception = exception
print(exception)
}
}
func clickButton(){
// buttonContext.evaluateScript("button.setTitleForState('JavaScriptButton','.Normal')")
let button = buttonContext.objectForKeyedSubscript("button")
//这里主要是注意函数名转换规则即可,然后传入需要的参数。
button.invokeMethod("setTitleForState", withArguments: ["JavaScriptButton",".Normal"])
}
当运行点击UIButton时就会看到button的title被改变了,效果如下:
推荐文章
1:https://www.raywenderlich.com/124075/javascriptcore-tutorial
2:http://nshipster.cn/javascriptcore/
3:http://ifelseboy.com/2015/11/05/javascriptcore/
4:http://blog.csdn.net/lizhongfu2013/article/details/9232129