iOS下JS与Swift互相调用(四)JavaScriptCore

这篇文章讲一下 JavaScriptCore,它是从 iOS7 开始加入的,这个框架其实只是基于 webkit 中以 C/C++ 实现的JavaScriptCore的一个包装,该框架让Objective-C、Swift 和 JavaScript 代码直接的交互变得更加的简单方便

关于 JavaScriptCore 的使用可以先看这两篇文章:

NSHipster 中文版的 Java​Script​Core

iOS7 新 JavaScriptCore 框架入门介绍

看了上面两篇文章应该对 JavaScriptCore 有了基本的了解

1、简要介绍JavaScriptCore

使用 JavaScriptCore 需要先导入框架 import JavaScriptCoreJavaScriptCore 主要有五个类:

  • JSContext
  • JSValue
  • JSManagedValue
  • JSVirtualMachine
  • JSExport

1.1 JSVirtualMachine

JSVirtualMachine 看名字直译是 JS 虚拟机,也就是说JavaScript是在一个虚拟的环境中执行,而 JSVirtualMachine 为其执行提供底层资源:

1
2
3
4
/*@interface
@discussion An instance of JSVirtualMachine represents a single JavaScript "object space" or set of execution resources. Thread safety is supported by locking the virtual machine, with concurrent JavaScript execution supported by allocating separate instances of JSVirtualMachine. */
NS_CLASS_AVAILABLE(10_9, 7_0)
@interface JSVirtualMachine : NSObject

一个 JSVirtualMachine 实例,代表一个独立的 JavaScript 对象空间,并为其执行提供资源,它通过加锁虚拟机,保证 JSVirtualMachine是线程安全的,如果要并发执行 JavaScript,那我们必须创建多个独立的 JSVirtualMachine 实例,在不同的实例中执行 JavaScript

通过 alloc/init 就可以创建一个新的 JSVirtualMachine 对象,但是我们一般不用新建 JSVirtualMachine 对象,因为创建 JSContext 时,如果我们不提供一个特性的 JSVirtualMachine,内部会自动创建一个 JSVirtualMachine 对象

1.2 JSContext和JSValue

JSVirtualMachineJavaScript 的运行提供了底层资源,JSContext 就为其提供着运行环境,JSContext 也管理 JSVirtualMachine 中对象的生命周期。每一个 JSValue 对象都要强引用关联一个 JSContext,当与某 JSContext 对象关联的所有 JSValue 释放后,JSContext也会被释放

创建一个JSContext对象的方式下面三种:

1
2
3
4
5
6
7
8
9
10
// 1.这种方式需要传入一个JSVirtualMachine对象,如果传nil,会导致应用崩溃的
let JSVM = JSVirtualMachine()
let JSCtx1 = JSContext(virtualMachine: JSVM)
// 2.这种方式,内部会自动创建一个JSVirtualMachine对象,可以通过JSCtx.virtualMachine
// 看其是否创建了一个JSVirtualMachine对象。
let JSCtx2 = JSContext()
// 3. 通过webView的获取JSContext。
let context = self.webView?.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext")

本文使用方式3来创建JSContext

JSValue 包括一系列方法用于访问其可能的值以保证有正确的 Foundation 类型,包括:

JavaScript Type JSValue method Objective-C Type Swift Type
boolean toBool BOOL Bool
number toNumber NSNumber NSNumber!
Date toDate NSDate NSDate!
Array toArray NSArray [AnyObject]!
Object toDictionary NSDictionary [NSObject : AnyObject]!
Object toObject custom type custom type

1.3 JSManagedValue

JSManagedValue 主要用途是解决 JSValue 对象在堆上的安全引用问题,把JSValue 保存进堆对象中是不正确的,这很容易引发循环引用,而导致JSContext不能释放,这个类主要是将JSValue对象转换为JSManagedValue的API,而且也不常用,就不做具体介绍了

1.4 JSExport

JSExport 是一个协议类,但是该协议并没有任何属性和方法,我们可以自定义一个协议类,继承自JSExport 无论我们在JSExport里声明的属性,实例方法还是类方法,继承的协议都会自动的提供给任何 JavaScript 代码,我们只需要在自定义的协议类中,添加上属性和方法就可以了

2 使用 JavaScriptCore 实现 JS 与 Swift 互调

2.1 创建 UIWebView,并加载本地HTML

这部分和之前的 iOS下JS与Swift互相调用(一)UIWebView拦截URL 一样:

1
2
3
4
5
6
7
8
9
10
11
12
func setupWebView() {
self.webView = UIWebView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height))
self.webView?.delegate = self
// 取消不想要webView 的回弹效果
self.webView?.scrollView.bounces = false
// UIWebView 滚动的比较慢,这里设置为正常速度
self.webView?.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal
let url = Bundle.main.url(forResource: "index.html", withExtension: nil)
let request = NSURLRequest(url: url!)
self.webView?.loadRequest(request as URLRequest)
self.view.addSubview(self.webView!)
}

HTML 的内容也大致一样,不过 JS 的调用有些区别:

1
2
3
4
5
6
7
8
9
10
<h1>这是按钮调用</h1>
<input type="button" value="扫一扫" onclick="JavaScriptCoreBridge.scanClick()" />
<input type="button" value="获取定位" onclick="JavaScriptCoreBridge.locationClick()" />
<input type="button" value="修改背景色" onclick="colorClick()" />
<input type="button" value="分享" onclick="shareClick()" />
<input type="button" value="支付" onclick="payClick()" />
<input type="button" value="摇一摇" onclick="JavaScriptCoreBridge.shake()" />
<input type="button" value="返回" onclick="JavaScriptCoreBridge.goBack()" />
<input type="button" value="输出arr" onclick="showArr()" />
<h1>这是文件上传</h1>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var shareClick = function () {
JavaScriptCoreBridge.shareContentUrl('测试分享的标题','测试分享的内容','http://www.baidu.com');
}
function setLocation(location) {
asyncAlert(location);
document.getElementById("returnValue").value = location;
}
function getQRCode(result) {
asyncAlert(result);
document.getElementById("returnValue").value = result;
}
function colorClick() {
var colorInfo = JSON.stringify({"red": "67", "green": "205", "blue": "12", "alpha":"0.5"});
JavaScriptCoreBridge.changeColor(colorInfo);
}
function payClick() {
var payInfo = JSON.stringify({"orderNo": "201511120981234", "channel": "wx", "amount": "12", "subject":"黑色毛衣"});
JavaScriptCoreBridge.pay(payInfo);
}

详细请看文章末尾的源码

2.2 添加 JS 要调用的原生 Swift 方法

JSContext 访问我们的 Swift 代码的方式主要有两种:block 和 JSExport 协议,这里有一个小问题,需要注意:block 方法仅仅适用于 OC 的block,并不适用于 Swift 中的闭包,为了公开闭包,我们将进行如下两步操作:

  • 使用@convention(block) 属性标记闭包,来建立桥梁成为 OC 中的block
  • 在映射block到JavaScript方法调用之前,我们需要unsafeBitCast函数将block转成为AnyObject

本文只讨论 JSExport 协议的方式使用 JavaScriptCore,在 webViewDidFinishLoad 代理方法里面注入一个叫 JavaScriptCoreBridge 的对象,这个对象用来充当原生应用和 Web 页面之间的一个桥梁

1
2
3
4
5
6
7
8
//MARK: - UIWebViewDelegate
func webViewDidFinishLoad(_ webView: UIWebView) {
let context = self.webView?.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext") as! JSContext
context.setObject(self, forKeyedSubscript: "JavaScriptCoreBridge" as NSCopying & NSObjectProtocol)
context.exceptionHandler = {context, exceptionValue in
print(exceptionValue ?? "")
}
}

自定义 SwiftJavaScriptDelegate 协议,而且此协议必须遵守 JSExport协议,自定义协议中的方法就是暴露给 Web 页面的方法,在 webView 加载完毕的时候获取 JS 运行的上下文环境,然后再注入桥梁对象名为JavaScriptCoreBridge,承载的对象为 self 即为此控制器,控制器遵守此自定义协议实现协议中对应的方法, 在 JS 调用完本地应用的方法做完相对应的事情之后,又回调了 JS 中对应的方法,从而实现了 Web 页面和本地应用之间的通讯:

1
2
3
4
5
6
7
8
9
10
11
@objc protocol SwiftJavaScriptDelegate: JSExport {
func scanClick()
func locationClick()
//多参数注意 js里面的方法的写法,需要在方法名里带参数名
func share(_ title: String, content: String, url:String)
//以下两个方法直接从js传json字符串,避免上面的问题
func changeColor(_ colorInfo: String)
func pay(_ payInfo: String)
func shake()
func goBack()
}

这里有两点需要注意:

  • 自定义协议必须使用 @objc,因为 JavaScriptCore 库是 Objective C版本的,如果不加 @objc,在 Swift 则调用无效果

  • 自定义协议里面的方法,如果是需要传多个参数(大于一个),JS 调用时就要对应的跟上参数的名字,第一个参数名可以省略,什么意思呢?看下面的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    // 这是协议里面的方法的写法
    //多参数注意 js里面的方法的写法,需要在方法名里带参数名
    func share(_ title: String, content: String, url:String)
    //这是 JS 里对应方法的写法
    var shareClick = function () {
    JavaScriptCoreBridge.shareContentUrl('测试分享的标题','测试分享的内容','http://
    www.baidu.com');
    }

上面第二点注意的原因我目前也不清楚,只能这样写才 JS 能顺利的调用 Swift 的方法,所以为了避免这些麻烦,我建议不管多少个参数,都改成传一个 Json 字符串,这样就避免了 JS 里方法的写法问题,demo 里面支付信息有多个参数我就是把多个参数转成一个 Json 字符串传给 Swift :

1
2
3
4
function payClick() {
var payInfo = JSON.stringify({"orderNo": "201511120981234", "channel": "wx", "amount": "12", "subject":"黑色毛衣"});
JavaScriptCoreBridge.pay(payInfo);
}

2.3 JS 和 Swift 的相互调用

上面的准备工作完成之后,JS 和 Swift 的相互调用就很简单了,控制器遵守自定义的 SwiftJavaScriptDelegate 的协议,并实现协议里的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func share(_ title: String, content: String, url: String) {
let jsStr = "shareResult('\(title)', '\(content)', '\(url)')"
self.webView?.stringByEvaluatingJavaScript(from: jsStr)
}
func pay(_ payInfo: String) {
if !payInfo.isEmpty {
let payInfoData = payInfo.data(using: .utf8)
if let payInfoDic = try? JSONSerialization.jsonObject(with: payInfoData!, options: .mutableContainers) as! [String: String] {
print("支付信息:\(payInfoDic)")
//self.webView?.stringByEvaluatingJavaScript(from: "payResult('支付成功')")
//JSContext.current().evaluateScript("payResult('支付成功')")
JSContext.current().objectForKeyedSubscript("payResult").call(withArguments: ["支付成功"])
}
}
}

这样就完成了 JS 调用 Swift,Swift 调用 JS 的方式就有多种了:

  • 方式一:JSContext 的 evaluateScript 方法

    1
    JSContext.current().evaluateScript("payResult('支付成功')")
  • 方式二:使用 JSValue 的 callWithArguments 方法

    1
    JSContext.current().objectForKeyedSubscript("payResult").call(withArguments: ["支付成功"])
  • 方式三:之前介绍的 UIWebView 的 stringByEvaluatingJavaScript 方法

    1
    self.webView?.stringByEvaluatingJavaScript(from: "payResult('支付成功')")

3 JavaScriptCore 的补充说明

使用JavaScriptCore,JS调用Native方法时,参数的传递更方便,不用担心特殊符号的转换问题,由于WKWebView 不支持通过如下的 KVC 的方式创建 JSContext:

1
let context = self.webView?.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext") as! JSContext

所以就不能在 WKWebView 中使用 JavaScriptCore,但是 WKWebView 可以使用WKScriptMessageHandler 的方式和 JS 交互,这种方式更加简单方便

原文地址: iOS 下 JS 与 OC 互相调用(四)– JavaScriptCore

原文是讨论 OC 与 JS 的交互,我按照作者的思路,用 Swift 实现 Native 与 JS 的交互,这是 代码地址

更多参考文章:

iOS 开发 - Swift 使用 JavaScriptCore 与 JS 交互

Objective-C 与 JavaScript 交互的那些事

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

本人刚开始写博客,主要是为了给自己的知识点做一个笔记,方便自己以后查阅,如果能让别人有所启发也是荣幸之至!如有错误,欢迎指正!