JavaScriptCore基本概念和基本使用(Swift)

时间:2023-01-23 23:16:09

JavaScriptCore简介


     iOS 7中加入了JavaScriptCore框架,该框架让Objective-CJavaScript代码直接交互变得更加简单方便。JavaScriptCoreCocoa API,用于建立JavaScript Swift or Objective- C的桥梁。该框架允许我们在基于Swift or Objective- C的程序中执行 JavaScript程序,也可以插入自定义对象到JavaScript环境中。 JavaScriptCore框架对外暴露的类非常少,只有5个类,分别是JSContextJSValueJSManagedValueJSVirtualMachineJSExport,其中最核心的是JSContextJSValue。下面一起来看一下几个部分:


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中。如下图所示:


JavaScriptCore基本概念和基本使用(Swift)

 JSContext也用于管理JavaScript虚拟机中对象的生命周期,每一个JSValue实例都将与JSContext通过强引用相关联。只要JSValue存在,JSContext就会保持引用,当JSContext中所有的JSValue被释放掉,那么JSContext也将会被释放,除非之前有被retained


 JSValue


     JSValue可以说是JavaScriptObject-CSwift之间数据互换的桥梁。为了在原生代码(native code)JavaScript代码之间传递数据,JSContext里的不同的Javascript值都可以封装在JSValue的对象里,包括字符串、数值、数组、函数等,甚至还有Error以及nullundefined;同时这个类型的对象可以方便快速地转化为swift里常用的数据类型,如toBool()toInt32()toArray()toDictionary()等。我们也可以使用JSValue创建JavaScript对象来包装原生自定义类中的对象,或者通过原生的方法或block来提供JavaScript函数的实现。


     每一个JSValue实例都是来自于JSContextJSValue则包含了对context对象的强应用,这点需要特别注意,如果不注意可能会造成内存泄露。当我们通过JSValue调用方法时,返回的新JSValue跟之前的JSValue是属于同一个context的。


      每一个JavaScript值也与具体的JSVirtualMachine对象相关联(间接通过context属性),该对象拥有这底层环境执行的资源。JSValue只能在同一个JSVirtualMachine之传递,如果传递到另外一个JSVirtualMachine,就会产生一个OC异常。之前的图片和相关内容也有提到过。


Converting Between JavaScript and Native Types 具体转换如下图:


JavaScriptCore基本概念和基本使用(Swift)


 JSExport 

 

      这是一个协议而不是对象。正如名字含义一样,我们可以使用这个协议暴露原生对象,实例方法,类方法,和属性给JavaScript,这样JavaScript就可以调用相关暴露的方法和属性。遵守JSExport协议,就可以定义我们自己的协议,在协议中声明的API都会在JS中暴露出来。

 

Exporting Objective-C Objects to JavaScript


      当通过OC实例创建一个JavaScript Value的时候,并且JSValue类没有特别指明赋值协议(copying convention),JavaScriptCore会通过创建一个JavaScript Wrapper Objectwrap这个OC实例。(对于特定的类,JavaScriptCore会自动赋值values到适当的JavaScript类型,例如,NSString instances 变成 JavaScript strings)


      在JS中,继承是通过原型链来实现的。对于每个输出(export)OC类,JavaScriptCore都会在对应的JavaScript context中创建一个原型(prototype)。对于NSObject类对应的prototype objectJavaScript ContextObject 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协议暴露属性和方法,例2JavaScript中进行使用。


JavaScriptCore基本概念和基本使用(Swift)

JavaScriptCore基本概念和基本使用(Swift)

解释:OC中的属性声明确定与之对应的JavaScript属性:


如果OC中的属性被声明为readonlyJavaScript属性将有属性writable: false, enumerable: false, configurable: true.

如果OC中的属性被声明为readwriteJavaScript属性将有属性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将采用下列转换生成对应的函数名:


1All colons are removed from the selector.    selector中所有的冒号都将被去除。

2Any 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,我们使用下列声明:

JavaScriptCore基本概念和基本使用(Swift)

注意:JSExportAs仅仅适用于selector拥有一个或者多个参数。


 JSVirtualMachine


     一个JSVirtualMachine实例代表着JavaScript执行的自包含环境。使用这个类有两个目的:支持JavaScript并发执行、为JavaScriptObjective-CSwift的过渡添加管理内存对象


 Threading and Concurrent JavaScript Execution

 

       每一个JavaScript context(一个JSContext对象)都属于一个虚拟机(virtual machine).每一个虚拟机都能够包含多个contexts,并且允许在两个context之间传递values(一个JSValue对象)。然而,每一个虚拟机又是独特的,我们不能够传递一个value从一个虚拟机的context到另外一个虚拟机的context

 

 JavaScriptCore API是线程安全的,比如:我们能够在任意线程创建一个JSValue对象,或者执行scriptevaluate 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 valueunderlying 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 valuevalue属性将被自动置 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

         JavaScriptCore基本概念和基本使用(Swift)


 2:在testLocal.js文件中加入以下代码,计算阶乘


JavaScriptCore基本概念和基本使用(Swift)


 3JavaScript中调用原生代码,可以通过两种方式:

    

    1Block方式JS functions

    2JSExport protocol遵守协议:JS objects


block方式:

    

       js中运行原生代码,一种方法那就是block,他们将自动与JavaScript方法建立桥梁,然后,这里有一个小问题,需要注意:这种方法仅仅适用于OCblock。并不适用于swift中的闭包。为了公开闭包,我们将进行如下两步操作:

    

    1)使用@convention(block) 属性标记闭包,来建立桥梁成为OC中的block

    2)在映射blockJavaScript方法调用之前,我们需要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的注意事项

            

        从上面例子应该有体会到BlockJavaScriptCore中起到的强大作用,它在JavaScriptObjective-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. 类中是属性将作为原型中的访问器属性。

     3For 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
}

4JavaScript与原生控件的交互

//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时就会看到buttontitle被改变了,效果如下:


JavaScriptCore基本概念和基本使用(Swift)

JavaScriptCore基本概念和基本使用(Swift)


推荐文章


1https://www.raywenderlich.com/124075/javascriptcore-tutorial

2http://nshipster.cn/javascriptcore/

3http://ifelseboy.com/2015/11/05/javascriptcore/

4:http://blog.csdn.net/lizhongfu2013/article/details/9232129