浅谈JavaScriptCore

一. JavaScriptCore 简介

1.1 JavaScriptCore 和 JavaScriptCore 框架

首先要区分JavaScriptCore 和 JavaScriptCore 框架(同后文中的JSCore)

JavaScriptCore框架 是一个苹果在iOS7引入的框架,该框架让 Objective-C 和 JavaScript 代码直接的交互变得更加的简单方便。

而JavaScriptCore是苹果Safari浏览器的JavaScript引擎,或许你听过Google的V8引擎,在WWDC上苹果演示了最新的Safari,据说JavaScript处理速度已经大大超越了Google的Chrome,这就意味着JavaScriptCore在性能上也不输V8了。

JavaScriptCore框架其实就是基于webkit中以C/C++实现的JavaScriptCore的一个包装,在旧版本iOS开发中,很多开发者也会自行将webkit的库引入项目编译使用。现在iOS7把它当成了标准库。

JavaScriptCore框架在OS X平台上很早就存在的,不过接口都是纯C语言的,而在之前的iOS平台(iOS7之前),苹果没有开放该框架,所以不少需要在iOS app中处理JavaScript的都得自己从开源的WebKit中编译出JavaScriptCore.a,接口也是纯C语言的。可能是苹果发现越来越多的程序使用了自编译的JavaScriptCore,干脆做个顺水人情将JavaScriptCore框架开放了,同时还提供了Objective-C的封装接口。

本篇文章将要讨论的就是基于Objective-C封装的JavaScriptCore框架,也就是我们开发iOS app时使用的JavaScriptCore框架。

二. Objective-C 与 JavaScript 交互

先看一个小的demo:

浅谈JavaScriptCore_第1张图片
屏幕快照 2017-04-25 下午5.30.42.png

很简单的几行代码,首先,我们引入了JavaScriptCore框架,然后创建了一个叫JSContext的类的对象,再然后用这个JSContext执行了一个段JS代码2 + 2,这里的JS代码是以字符串的形式传入的,执行后得到一个JSValue类型的值,最后,将这个JSVlaue类型的值转换成整型并输出。
输出结果如下,这样我们就用OC调用了一段JS代码,执行结果如下:


屏幕快照 2017-04-25 下午5.33.40.png

这个 demo 里面出现了2个之前没见过的类,一个叫JSContext,一个叫JSValue,下面我们一个一个说下。

2.1JSContext
  • SContext 是JS代码的执行环境
    JSContext 为JS代码的执行提供了上下文环境,通过jSCore执行的JS代码都得通过JSContext来执行。
  • JSContext对应于一个 JS 中的全局对象
    JSContext对应着一个全局对象,相当于浏览器中的window对象,JSContext中有一个GlobalObject属性,实际上JS代码都是在这个GlobalObject上执行的,但是为了容易理解,可以把JSContext等价于全局对象。

你可以把他想象成这样:


浅谈JavaScriptCore_第2张图片
JSContext
2.2 JSValue
  • JSValue 是对 JS 值的包装
    JSValue 顾名思义,就是JS值嘛,但是JS中的值拿到OC中是不能直接用的,需要包装一下,这个JSValue就是对JS值的包装,一个JSValue对应着一个JS值,这个JS值可能是JS中的number,boolean等基本类型,也可能是对象,函数,甚至可以是undefined,或者null。如下图:


    浅谈JavaScriptCore_第3张图片
    OC-JS类型对照表

    其实,就相当于JS 中的 var。

  • JSValue存在于JSContext中
    JSValue是不能独立存在的,它必须存在于某一个JSContext中,就像浏览器中所有的元素都包含于Window对象中一样,一个JSContext中可以包含多个JSValue。就像这样:


    浅谈JavaScriptCore_第4张图片
    JSValue

    Tips: 图中的 λ (lambda) 符号表示匿名函数,闭包的意思,它的大写形式为 ^ ,这就是为什么 OC 中 Block 定义都有一个 ^ 符号。

  • 都是强引用
    这点很关键,JSValue对其对应的JS值和其所属的JSContext对象都是强引用的关系。因为jSValue需要这两个东西来执行JS代码,所以JSValue会一直持有着它们。

下面这张图可以更直观的描述出它们之间的关系:


浅谈JavaScriptCore_第5张图片
762048-066c0f5be1ae8762.png

通过下面这些方法来创建一个JSValue对象:


浅谈JavaScriptCore_第6张图片

你可以将OC中的类型,转换成JS中的对应的类型(参见前面那个类型对照表),并包装在JSValue中,包括基本类型,Null和undfined。

或者你也可以创建一个新的对象,数组,正则表达式,错误,这几个方法达到的效果就相当于在JS中写 var a = new Array();

也可以将一个OC对象,转成JS中的对象,但是这样转换后的对象中的属性和方法,在JS中是获取不到的,怎样才能让JS中获取的OC对象中的属性和方法,我们后面再说。

2.3 实际使用

再看一个Demo:
首先是一段JS代码,一个简单的递归函数,计算阶乘的:


浅谈JavaScriptCore_第7张图片
JS中计算阶乘的代码

然后,如果我们想在OC中调用这个JS中的函数该如何做呢?如下:


浅谈JavaScriptCore_第8张图片
OC调用JS的代码

首先,从bundle中加载这段JS代码。

然后,创建一个JSContext,并用他来执行这段JS代码,这句的效果就相当于在一个全局对象中声明了一个叫fatorial的函数,但是没有调用它,只是声明,所以执行完这段JS代码后没有返回值。

注意:evaluateScript只是执行了一段代码,对一段JS代码(转成NSString)估值,如果是函数的情况下,并不代表它会执行函数内容,evaluateScript进行的只是函数名字的声明,以及函数内容的获取,但未执行。evaluateScript后如果执行的不是函数而是一段JS源码,那么就是简单的执行。此时JS代码中的所以函数都被全局对象声明了。

再从这个全局对象中获取这个函数,这里我们用到了一种类似字典的下标写法来获取对应的JS函数,就像在一个字典中取这个key对应的value一样简单,实际上,JS中的对象就是以 key : Value 的形式存储属性的,且JS中的object对象类型,对应到OC中就是字典类型,所以这种写法自然且合理。

这种类似字典的下标方式不仅可以取值,也可以存值。不仅可以作用于Context,也可以作用与JSValue,他会用中括号中填的key值去匹配JSValue包含的JS值中有没有对应的属性字段,找到了就返回,没找到就返回undefined。

然后,我们拿到了包装这个阶乘函数的的JSValue对象,在其上调用callWithArguments方法,即可调用该函数,这个方法接收一个数组为参数,这是因为JS中的函数的参数都不是固定的,我们构建了一个数组,并把NSNumber类型的5传了过去,然而JS肯定是不知道什么是NSNumber的,但是别担心,JSCore会帮我们自动转换JS中对应的类型, 这里会把NSNumber类型的5转成JS中number类型的5,然后再去调用这个函数(这就是前面说的API目标中自动化的体现)。

最后,如果函数有返回值,就会将函数返回值返回,如果没有返回值则返回undefined,当然在经过JSCore之后,这些JS中的类型都被包装成了JSValue,最后我们拿到返回的JSValue对象,转成对应的类型并输出。这里结果是120,我就不贴出来了。

以上就是利用JavascriptCore在OC中调用JS代码里的函数的方法。

三. JavaScript 与 Objective-C 交互(js调用OC的方法)

JavaScript 与 Objective-C 交互主要通过2种方式:

  • Block : 第一种方式是使用block,block也可以称作闭包和匿名函数,使用block可以很方便的将OC中的单个方法暴露给JS调用,具体实现我们稍后再说。
  • JSExport 协议 : 第二种方式,是使用JSExport协议,可以将OC的中某个对象直接暴露给JS使用,而且在JS中使用就像调用JS的对象一样自然。
    简而言之,Block是用来暴露单个方法的,而JSExport 协议可以暴露一个OC对象,下面我们详细说一下这两种方式。
3.1 Block

上面说过,使用Block可以很方便的将OC中的单个方法(即Block)暴露给JS调用,JSCore会自动将这个Block包装成一个JS方法,具体怎么个包装法呢?上Demo:


浅谈JavaScriptCore_第9张图片
Block_Demo

这就是一段将OC Block暴露给JS的代码,很简单是不是,就像这样,我们用前面提过的这种类似字典的写法把一个OC Bock注入了context中,这个block接收一个NSDictionary类型的参数,并返回了一个UIColor类型的对象
这样写的话,会发生什么呢?请看下图

浅谈JavaScriptCore_第10张图片

我们有一个JSContext,然后将一个OCBlock注入进去,JSCore会自动在全局对象中(因为是直接在Context上赋值的,context对应于全局对象)创建一个叫makeNSColor的函数,将这个Block包装起来。

个人理解:context[@"makeNSColor"] = block;相当于在context中即JS中创建了一个名字叫makeNSColor的函数,可以供JS调用,理论上也可以供OC去调用该JS函数。只不过该函数的内容需要block去包装。

然后,在JS中,我们来调用这个暴露过来的block,其实直接调用的是那个封装着Block的MakeNSColor方法。
下面是JS代码里调用该函数


浅谈JavaScriptCore_第11张图片

浅谈JavaScriptCore_第12张图片
调用OC中写的JS函数

这里有一个叫colorForWord的JS方法,它接收一个word参数,这个colorMap是一个JS对象,里面按颜色名字保存着一些色值信息,这些色值信息也是一个个的JS对象,这个ColorForWord函数就是通过颜色名字来取得对应的颜色对象。然后这函数里面调用了MakeNSColor方法,并传入从colorMap中根据word字段取出来的颜色对象,注意这个颜色对象是一个JS对象,是一个object类型,但是我们传进来的Block接收的是一个NSDIctionary类型的参数啊,不用担心,这时JSCore会自动帮我们把JS对象类型转成NSDictionary类型,就像前面那个表里写的一样,NSDictionary对应着JS中的Object类型。
现在,我们有一个包装着Block的JS函数makeNSColor,然后又有一个colorForWrod函数来调用它,具体过程就像这样:


浅谈JavaScriptCore_第13张图片

图从左边看起,colorForWrod调用makeNSColor,传过去的参数是JS Object类型(从colorMap中取出的颜色对象),JSCore会将这个传过来的Object参数转换成NSDictionary类型,然后makeNSColor用其去调用内部包装的Block,Block返回一个UIColor(NSObject)类型的返回值,JScore会将其转换成一个wrapper Object(其实也是JS Object类型),返回给colorForWrod。

如果我们在OC中调用这个colorForWrod函数,会是什么样子呢?如下图:

浅谈JavaScriptCore_第14张图片
OC中调用带有OCblock包装的

OC Caller去调用这个colorForWrod函数,因为colorForWrod函数接收的是一个String类型那个参数word,OC Caller传过去的是一个NSString类型的参数,JSCore转换成对应的String类型。然后colorForWrod函数继续向下调用,就像上面说的,知道其拿到返回的wrapper Object,它将wrapper Object返回给调用它的OC Caller,JSCore又会在这时候把wrapper Object转成JSValue类型,最后再OC中通过对JSValue调用对应的转换方法,即可拿到里面包装的值,这里我们调用- toObject方法,最后会得到一个NSColor对象,即从最开始那个暴露给JS的Block中返回的对象。

通过一步一步的分析,我们发现,JavaScriptCore会在JS与OC交界处传递数据时做相应的类型转换,转换规则如前面的OC-JS类型对照表。
3.1.1 使用 Block 的坑

使用Block暴露方法很方便,但是有2个坑需要注意一下:

  • 不要在Block中直接使用JSValue
  • 不要在Block中直接使用JSContext
    因为Block会强引用它里面用到的外部变量,如果直接在Block中使用JSValue的话,那么这个JSvalue就会被这个Block强引用,而每个JSValue都是强引用着它所属的那个JSContext的,这是前面说过的,而这个Block又是注入到这个Context中,所以这个Block会被context强引用,这样会造成循环引用,导致内存泄露。不能直接使用JSContext的原因同理。


    浅谈JavaScriptCore_第15张图片

    那怎么办呢,针对第一点,建议把JSValue当做参数传到Block中,而不是直接在Block内部使用,这样Block就不会强引用JSValue了。
    针对第二点,可以使用[JSContext currentContext] 方法来获取当前的Context。

3.2 JSExport 协议
3.2.1 介绍

然后是JS和OC交互的第二种方式:JSExport 协议,通过JSExport 协议可以很方便的将OC中的对象暴露给JS使用,且在JS中用起来就和JS对象一样。

3.2.2 使用

举个栗子,我们在Objective-C中有一个MyPoint类,它有两个double类型的属性,x,y,一个实例方法description 和一个类方法 makePointWithX: Y:


浅谈JavaScriptCore_第16张图片

如果我们使用JSExport协议把这个类的对象暴露给JS,那么在JS中,我们怎么使用这个暴露过来的JS对象呢?他的属性可以直接调用,就像调用JS对象的属性一样,他的实例方法也可以直接调用,就像调用JS对象中的方法一样,然后他的类方法,也可以直接用某个全局对象直接调用。就像普通的JS一样,但是操作的却是一个OC对象。


浅谈JavaScriptCore_第17张图片

实现这些只需要写这样一句话。
@protocol MyPointExports 

声明一个自定义的协议并继承自JSExport协议。然后当你把实现这个自定义协议的对象暴露给JS时,JS就能像使用原生对象一样使用OC对象了,也就是前面说的API目标之高保真。


浅谈JavaScriptCore_第18张图片

需要注意的是,OC中的函数声明格式与JS中的不太一样(应该说和大部分语言都不一样。。),OC函数中多个参数是用冒号:声明的,这显然不能直接暴露给JS调用,这不高保真。。

所以需要对带参数的方法名做一些调整,当我们暴露一个带参数的OC方法给JS时,JSCore会用以下两个规则生成一个对应的JS函数:

  • 移除所有的冒号
  • 将跟在冒号后面的第一个小写字母大写
    比如上面的那个类方法,转换之前方法名应该是 makePointWithX:y:,在JS中生成的对应的方法名就会变成 makePointWithXY。

苹果知道这种不一致可能会逼死某些强迫症。。所以加了一个宏JSExportAs来处理这种情况,它的作用是:给JSCore在JS中为OC方法生成的对应方法指定名字。

比如,还是上面这个方法makePointWithX:y:,可以这样写:



这个makePoint就是给JS中方法指定的名字,这样,在JS中就能直接调用makePoint来调用这个OC方法makePointWithX:y:了。

注意:这个宏只对带参数的OC方法有效。

3.2.3 探究

但是,光会用可不行,这个JSExoprt协议到底做了什么呢?

当你声明一个继承自JSExport的自定义协议时,就是在告诉JSCore,这个自定义协议中声明的属性,实例方法和类方法需要被暴露给JS使用。(不在这个协议中的方法不会被暴露出去。)

当你把实现这个协议的类的对象暴露给JS时,JS中会生成一个对应的JS对象,然后,JSCore会按照这个协议中声明的内容,去遍历实现这个协议的类,把协议中声明的属性,转换成JS 对象中的属性,实质上是转换成getter 和 setter 方法,转换方法和之前说的block类似,创建一个JS方法包装着OC中的方法,然后协议中声明的实例方法,转换成JS对象上的实例方法,类方法转换成JS中某个全局对象上的方法。


浅谈JavaScriptCore_第19张图片

你可能感兴趣的:(浅谈JavaScriptCore)