iOS与JS交互

iOS中调用JS

先写JS操作语句,再通过IOS中的webView执行这个条JS语句。
使用UIwebView调用的是stringByEvaluatingJavaScript(from:jsStr)这个方法
使用WKWebView调用的是evaluateJavaScript(jsStr) { (result, error) in }

示例:增删改查

1
2
3
4
5
6
7
8
9
<body>
<p id="title">我的水果清单</p>
<ul>
<li class="change">苹果</li>
<li>香蕉</li>
<li>草莓</li>
<li>西瓜</li>
</ul>
</body>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 删除标题
let str1 = "document.getElementById('title').remove()"
webView.stringByEvaluatingJavaScript(from: str1)
// 修改
let str2 = "var change = document.getElementsByClassName('change')[0];"
let str3 = "change.innerHTML = '榴莲';"
print(str2)
webView.stringByEvaluatingJavaScript(from: str2 + str3)
// 插入
let str4 = "var img = document.createElement('img');"
let str5 = "img.src = 'xiniu.jpg';"
let str6 = "document.body.appendChild(img);"
webView.stringByEvaluatingJavaScript(from: str4 + str5 + str6)
// 查
let str7 = "document.body.outerHTML"
let bodyStr = webView.stringByEvaluatingJavaScript(from: str7)
print(bodyStr ?? "")

JS调用iOS

利用WKWebView的新特性MessageHandler来实现JS调用原生方法带来的好处:
1、在JS中写起来简单,不用再用创建URL的方式那么麻烦了
2、JS传递参数更方便。使用拦截URL的方式传递参数,只能把参数拼接在后面,如果遇到要传递的参数中有特殊字符,如&、=、?等,必须得转换,否则参数解析肯定会出错。
示例:在html页面点击登录按钮,把账号和密码传给iOS端;点击html页面的访问相册按钮,访问手机系统相册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<body>
<p></p>
<span>用户名</span>
<input id="username">
<p></p>
<span>密码</span>
<input id="pwd" type="password">
<p></p>
<button id="loginBtn">登录</button>
<p></p>
<button id="photoBtn">访问相册</button>
<script>
var username = document.getElementById('username');
var pwd = document.getElementById('pwd');
var loginBtn = document.getElementById('loginBtn');
var photoBtn = document.getElementById('photoBtn');
loginBtn.onclick = function(){
window.webkit.messageHandlers.loginInfo.postMessage({'username':username.value,'password':pwd.value})
}
photoBtn.onclick = function(){
window.webkit.messageHandlers.getPhoto.postMessage('showPhotos');
}
</script>
</body>

遵守协议WKScriptMessageHandler,UINavigationControllerDelegate,UIPickerViewDelegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
var webView :WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let url = Bundle.main.url(forResource: "index", withExtension: "html")!
let request = URLRequest(url: url)
let config = WKWebViewConfiguration()
let pf = WKPreferences()
pf.javaScriptCanOpenWindowsAutomatically = true
pf.minimumFontSize = 40
config.preferences = pf
// name:这个参数可以自定义,需要和js里messageHandler对应即可
// window.webkit.messageHandlers.loginInfo.postMessage()
// window.webkit.messageHandlers.getPhoto.postMessage()
config.userContentController.add(self, name: "loginInfo")
config.userContentController.add(self, name: "getPhoto")
webView = WKWebView(frame: view.frame, configuration: config)
webView.load(request)
view.addSubview(webView)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
webView.configuration.userContentController.removeScriptMessageHandler(forName: "loginInfo")
webView.configuration.userContentController.removeScriptMessageHandler(forName: "getPhoto")
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "loginInfo" {
if let dic = message.body as? NSDictionary{
print(dic["username"] ?? "")
print(dic["password"] ?? "")
}
}else if message.name == "getPhoto" {
if message.body as? String == "showPhotos"{
showPhotos()
}
}
}
func showPhotos(){
let imgCtr = UIImagePickerController()
imgCtr.sourceType = .photoLibrary
self.present(imgCtr, animated: true, completion: nil)
}

JavaScriptCore框架

JavaScriptCore框架其实就是基于webkit中以C/C++实现的JavaScriptCore的一个包装,
还提供了Objective-C的封装接口,不过只能使用在iOS 7以上。
(JavaScriptCore是苹果Safari浏览器的JavaScript引擎,类似于Google的V8引擎)

主要的类

1、JSContext — 在OC中创建JavaScript运行的上下文环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (instancetype)init;
// 在特定的对象空间上创建JSContext对象,获得JavaScript运行的上下文环境
- (instancetype)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;
// 运行一段js代码,输出结果为JSValue类型
- (JSValue *)evaluateScript:(NSString *)script;
- (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)sourceURL;
// 获取当前正在运行的JavaScript上下文环境
+ (JSContext *)currentContext;
// 返回结果当前执行的js函数 function () { [native code] }
+ (JSValue *)currentCallee;
// 返回结果当前方法的调用者[object Window]
+ (JSValue *)currentThis;
// 返回结果为当前被调用方法的参数
+ (NSArray *)currentArguments;
// js的全局变量 [object Window]
@property (readonly, strong) JSValue *globalObject;

示例:创建JSContext

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

2、JSValue — 可以转成OC数据类型,JSContext强引用着JSValue
OC与JS数据类型对比、转换
Image text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 在context创建BOOL的JS变量
+ (JSValue *)valueWithBool:(BOOL)value inContext:(JSContext *)context;
// 将JS变量转换成OC中的BOOL类型
- (BOOL)toBool;
// 修改JS对象的属性的值
- (void)setValue:(id)value forProperty:(NSString *)property;
// JS中是否有这个对象
@property (readonly) BOOL isUndefined;
// 比较两个JS对象是否相等
- (BOOL)isEqualToObject:(id)value;
// 调用者JSValue对象为JS中的方法名称,arguments为参数,调用JS中Window直接调用的方法
- (JSValue *)callWithArguments:(NSArray *)arguments;
// 调用者JSValue对象为JS中的全局对象名称,method为全局对象的方法名称,arguments为参数
- (JSValue *)invokeMethod:(NSString *)method withArguments:(NSArray *)arguments;
// JS中的结构体类型转换为OC
+ (JSValue *)valueWithPoint:(CGPoint)point inContext:(JSContext *)context;

3、JSExport — 一个协议类,通过协议的方式把OC的属性和方法暴露给JS使用(自定义一个继承自JSExport协议的协议,并实现协议)

4、JSManagedValue — 主要用途是解决JSValue对象在Objective-C 堆上的安全引用问题。把JSValue 保存进Objective-C 堆对象中是不正确的,这很容易引发循环引用,而导致JSContext不能释放。这个类主要是将JSValue对象转换为JSManagedValue的API

1
2
JSManagedValue *jsManagedValue = [JSManagedValue managedValueWithValue:jsValue];
[_context.virtualMachine addManagedReference:jsManagedValue];

5、JSVirtualMachine — JS虚拟机,也就是说JavaScript是在一个虚拟的环境中执行,而JSVirtualMachine为其执行提供底层资源。有独立的堆空间和垃圾回收机制,运行在不同虚拟机环境的JSContext可以通过此类通信。
一个JSVirtualMachine实例,代表一个独立的JavaScript对象空间,并为其执行提供资源。它通过加锁虚拟机,保证JSVirtualMachine是线程安全的,如果要并发执行JavaScript,那我们必须创建多个独立的JSVirtualMachine实例,在不同的实例中执行JavaScript。
通过alloc/init就可以创建一个新的JSVirtualMachine对象。但是我们一般不用新建JSVirtualMachine对象,因为创建JSContext时,如果我们不提供一个特性的JSVirtualMachine,内部会自动创建一个JSVirtualMachine对象。

OC调用JS方法

示例一:OC调用JS(页内js)
方式1:使用JSContext的方法-evaluateScript

1
2
3
4
- (void)webViewDidFinishLoad:(UIWebView *)webView{
JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
[context[@"payResult"] callWithArguments:@[@"支付成功!"]];
}

方式2:使用JSValue的方法-callWithArguments

1
2
NSString *jsStr = [NSString stringWithFormat:@"payResult('%@')",@"支付成功!"];
[[JSContext currentContext] evaluateScript:jsStr];

示例二:OC调用JS(页外js)的平方方法
JS文件(square.js)的代码

1
2
3
var square = function (n) {
return n * n
};

OC中的代码

1
2
3
4
5
6
7
8
9
10
NSString * jsPath = [[NSBundle mainBundle] pathForResource:@"square.js" ofType:nil];
NSString * jsStr = [NSString stringWithContentsOfFile:jsPath encoding:NSUTF8StringEncoding error:nil];
JSContext *context = [[JSContext alloc] init];
[context evaluateScript:jsStr];
JSValue *jsFunction = context[@"square"];
JSValue *result = [jsFunction callWithArguments:@[@4]];
NSLog(@"square(4) = %d", [result toInt32]);

JS调用OC的方法

第一种使用block的方式:

将OC中的单个方法(即block)暴露给JS调用,JavaScriptCore框架会自动将这个Block包装成一个JS方法。
Image text
注意:使用black可能造成内存泄露的问题
不要在Block中直接使用JSValue,建议把JSValue当做参数传到Block中,这样Block就不会强引用JSValue了。
不要在Block中直接使用JSContext,可以使用[JSContext currentContext] 方法来获取当前的Context。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<header>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</header>
<body>
<button type="button" onclick="btnClick()">快点我呀</button>
<script type="text/javascript">
function btnClick() {
share('为了艾泽拉斯','为了联盟','为了部落');
}
</script>
</body>
</html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
NSURL *url = [[NSBundle mainBundle] URLForResource:@"jsScriCode" withExtension:@"html"];
[_webView loadRequest:[[NSURLRequest alloc] initWithURL:url]];
- (void)webViewDidFinishLoad:(UIWebView *)webView{
// 获取html页内js上下文
JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
context.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
NSLog(@"异常信息:%@", exceptionValue);
};
//js里调用share方法时就会调用这里的block内容
context[@"share"] = ^() {
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"js里调原生block" message:@"这是OC原生的弹窗" delegate:self cancelButtonTitle:@"收到" otherButtonTitles:nil];
[alertView show];
});
//获取js的share方法传过来的参数数组
NSArray *args = [JSContext currentArguments];
for (JSValue *jsVal in args) {
NSLog(@"%@", jsVal.toString);// 为了艾泽拉斯、为了联盟、为了部落
}
}
第二种使用JSExport协议的方式:

使用JSExport协议将OC中某个对象直接暴露给JS使用,而且在JS中使用就像调用JS的对象一样自然
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html>
<header>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script type="text/javascript">
var nativePhotosCallback = function(photos) {
alert(photos);
alert(NativeClassName.myName)
}
var shareBtnClick = function() {
var shareInfo = JSON.stringify({"title": "标题", "content": "内容", "url": "https://www.baidu.com"});
NativeClassName.shareBtnClick(shareInfo);
}
var nativeShareResultCallback = function(){
alert('success');
}
var btnClick = function() {
NativeClassName.macroFunction(18,1.82);
}
</script>
</header>
<body>
<button type="button" onclick="NativeClassName.cameraBtnClick()">调起相册</button>
<button type="button" onclick="shareBtnClick()">帮我分享一下</button>
<button type="button" onclick="btnClick()">以宏的方式调用</button>
</body>
</html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#import "ViewController.h"
#import <JavaScriptCore/JavaScriptCore.h>
@protocol JSExportDelegate <JSExport>
@property (nonatomic,copy) NSString * myName;
- (void)cameraBtnClick;
- (void)shareBtnClick:(NSString *)shareString;
JSExportAs (macroFunction,-(void)testMacroFunction:(NSInteger)age height:(CGFloat)height);
@end
@interface ViewController () <UIWebViewDelegate, JSExportDelegate>
@property (nonatomic, strong) JSContext *jsContext;
@property (nonatomic, strong) UIWebView * webView;
@end
@implementation ViewController
@synthesize myName;
- (void)viewDidLoad {
[super viewDidLoad];
self.myName = @"瓦里安.乌瑞恩";
UIWebView * webView = [[UIWebView alloc]initWithFrame:self.view.bounds];
webView.delegate = self;
NSURL *url = [[NSBundle mainBundle] URLForResource:@"texth5" withExtension:@"html"];
[webView loadRequest:[[NSURLRequest alloc] initWithURL:url]];
[self.view addSubview:webView];
}
#pragma mark - UIWebViewDelegate
- (void)webViewDidFinishLoad:(UIWebView *)webView {
self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
self.jsContext[@"NativeClassName"] = self;
self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
NSLog(@"异常信息:%@", exceptionValue);
};
}
#pragma mark - JSExportDelegate
- (void)cameraBtnClick{
NSLog(@"调起原生相册");
// 获取到照片后再回调js的nativePhotosCallback方法并把图片传过去
JSValue *phoCallback = self.jsContext[@"nativePhotosCallback"];
[phoCallback callWithArguments:@[@"photos"]];
// NSString *jsStr = [NSString stringWithFormat:@"nativePhotosCallback('%@')",@"photos"];
// [self.jsContext evaluateScript:jsStr];
}
- (void)shareBtnClick:(NSString *)shareString {
NSLog(@"分享内容:%@", shareString);
// 分享成功回调js的nativeShareResultCallback方法
JSValue *shareCallback = self.jsContext[@"nativeShareResultCallback"];
[shareCallback callWithArguments:nil];
}
-(void)testMacroFunction:(NSInteger)age height:(CGFloat)height{
NSLog(@"年龄:%zd,身高:%f",age,height);
}
@end

内存管理注意事项

1、不要在JS中给OC对象增加成员变量
2、OC对象不要直接强引用JSValue对象,解决方案:苹果推出了一种新的引用关系,叫conditional retain,有条件的强引用,JSManagedValue就是苹果用来实现conditional retain的类。JSManagedValue弱引用着JSValue
Image text

补充:
关于WKWebView 与JavaScriptCore,由于WKWebView 不支持通过如下的KVC的方式创建JSContext:

1
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

但WKWebView与JS交互有特有的方式:MessageHandler,在上文已介绍过。

多线程

说多线程之前得先说下另一个类JSVirtualMachine,它为JavaScript的运行提供了底层资源,有自己独立的堆栈以及垃圾回收机制。
JSVirtualMachine还是JSContext的容器,可以包含若干个JSContext,在一个进程中,你可以有多个JSVirtualMachine,里面包含着若干个JSContext,而JSContext中又有若干个JSValue。
需要注意的是,你可以在同一个JSVirtualMachine的不同JSContext中,互相传递JSValue,但是不能再不同的JSVirtualMachine中的JSContext之间传递JSValue。
Image text
这是因为,每个JSVirtualMachine都有自己独立的堆栈和垃圾回收器,一个JSVirtualMachine的垃圾回收器不知道怎么处理从另一个堆栈传过来的值。

JavaScriptCore提供的API本身就是线程安全的,你可以在不同的线程中,创建JSValue,用JSContext执行JS语句,但是当一个线程正在执行JS语句时,其他线程想要使用这个正在执行JS语句的JSContext所属的JSVirtualMachine就必须得等待,等待前前一个线程执行完,才能使用这个JSVirtualMachine。

当然,这个强制串行的粒度是JSVirtualMachine,如果你想要在不用线程中并发执行JS代码,可以为不同的线程创建不同JSVirtualMachine。

打赏支持一下呗!