频道栏目
首页 > 资讯 > JavaScript > 正文

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

16-06-13        来源:[db:作者]  
收藏   我要投稿

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并发执行、为JavaScriptObjective-CSwift的过渡添加管理内存对象

 

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文件中加入以下代码,计算阶乘

 

 

 

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

 

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
  }
 

4JavaScript与原生控件的交互

 

//MARK:对系统类的操作
@objc protocol UIButtonJSExport:JSExport{
    func setTitle(title: String,forState: UIControlState)
}

func exampleSystemControl(){
        
        //注意:需要在oc和swift混编的桥梁文件中导入 #import ,否则得不到运行时添加协议方法。
        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

 

 


相关TAG标签
上一篇:Android多线程编程之线程池学习篇(一)
下一篇:Android初级教程理论知识(第四章内容提供器)
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站