OC与js的交互 - javascriptCore

目录
  1. 1. 引言
    1. 1.0.1. javascriptCore 实战

引言

鉴于很多小伙伴最近老是问我关于js与OC交互的问题,原本打算先写完CoreAnimation的我,决定先吧这块知识点整理出来,毕竟核心动画的知识的确比较多,不是那么容易写完。

#OC与js的交互方式

OC与js的交互方式有好几种,不借助第三方框架的情况下,我们有两个选择,webkit和javascriptCore。

##方法一:通过webkit进行OC与js的交互
没认识JavaScriptCore之前,如果想在OC中使用JavaScript代码,一般都是在webview(webview内置webkit引擎,解析JavaScript代码)

主要方法就是在webView的代理中执行:
stringByEvaluatingJavaScriptFromString:@"JS的方法名"这个方法,即可在webView中调用js的函数
同时,通过代理方法webView:shouldStartLoadWithRequest: navigationType:监听由js在内部定义的类似重定向的消息,并在OC中执行相关的代码

代码如下:

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
#import "ViewController.h"
@interface ViewController ()<UIWebViewDelegate>
@property (nonatomic, strong)UIWebView * webView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.title = @"Web Test";
[self.view addSubview:self.webView];
}
-(UIWebView *)webView{
if (!_webView) {
_webView = [[UIWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
_webView.delegate = self;
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@".html"];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:filePath]];
[_webView loadRequest:request];
}
return _webView;
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
// 判断js是否需要OC调用相关的方法.
if ([request.URL.absoluteString hasSuffix:@"clickLoginBtn"]) {
// 向js传递数据
[_webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"loginCallBack('I am id from app','I am token from app')"]];
// 从js获取数据
NSString *webData = [_webView stringByEvaluatingJavaScriptFromString:@"returnData();"];
NSLog(@"%@",webData);
[_webView stopLoading];
}
return YES;
}

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
28
29
30
<!DOCTYPE HTML>
<html>
<head>
<script>
function loginCallBack(id,token){
var x=document.getElementById("logindata");
alert(id)
alert(token)
}
</script>
</head>
<body>
<h2 id="logindata" align= center>Clict To Transmit Data</h2>
<button id="hello" onclick="buttonClick()" >login</button>
<script >
function buttonClick()
{
         //webview重定向
document.location = "clickLoginBtn"
}
function returnData(){
return document.getElementById("logindata").script
}
</script>
</body>
</html>

这种方式可以解决大多数不需要太过复杂的OC与js交互的需求,但对于比较复杂,或者OC与js交互频繁的情况下,更推荐大家使用方法二。

##方法二:通过javascriptCore进行OC与js的交互

###javascriptCore简介
这是javascriptCore的头文件
这里写图片描述
在头文件中,我们可以看到,javascriptCore中有5个类,分别是

  • JSVirtualMachine
    JSVirtualMachine顾名思义,是javaScript的虚拟机,是为JSContext提供运行资源。

  • JSManagedValue
    主要是作为一个引用桥接,将 JSValue 转为 JSManagedValue 类型后,可以添加到 JSVirtualMachine 对象中,这样能够保证你在使用过程中 JSValue 对象不会被释放掉,当你不再需要该 JSValue 对象后,从 JSVirtualMachine 中移除该 JSManagedValue 对象,JSValue 对象就会被释放并置空。

  • JSContext
    JSVirtualMachine为JavaScript的运行提供了底层资源,JSContext就为其提供着运行环境。通过- (JSValue )evaluateScript:(NSString )script;方法就可以执行一段JavaScript脚本,并且如果其中有方法、变量等信息都会被存储在其中以便在需要的时候使用。而JSContext的创建都是基于JSVirtualMachine:- (id)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;,如果是使用- (id)init;进行初始化,那么在其内部会自动创建一个新的JSVirtualMachine对象然后调用前边的初始化方法。

  • JSValue
    JSValue则可以说是JavaScript和Object-C之间互换的桥梁,它提供了多种方法可以方便地把JavaScript数据类型转换成Objective-C,或者是转换过去。

  • JSExport
    JSExport是一个协议,让JSContext运行环境中的JavaScript 可以识别该协议中定义的实例方法、类方法、属性等,让objective-c/swift与JavaScript能够自动交互;

###通过OC执行js方法或调取js属性。
下面通过JSContext的evaluateScript方法模拟执行js代码,写一些简单的demo。
例子:

1
2
3
4
5
6
JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"var arr = [1, 2, 'This is js string'];var sum = function(a, b) { return a+b;}"];
JSValue *jsArray = context[@"arr"];
JSValue *jsSum = context[@"sum"];
JSValue *jsSumResult = [jsSum callWithArguments:[NSArray arrayWithObjects:@12, @33, nil]];

示例代码里的,JSValue *jsArray,jsArray对应着javaScript中的一个 array对象:arr。所以我们可以对jsArray进行一些操作,从而操作javaScript 中的 “arr”。

例如:
jsArray[0];//1
jsArray[2];//This is js string
jsArray[1] = 49;//修改arr 的第二个元素。
jsArray[@”length”];//结果是3,调用js arr对象的方法。

又比如我们示例代码里的,jsSum,对应着sum function,因此我们可以通过操作jsSum从而调取javaScript中的 “sum function”。

1
JSValue *jsSumResult = [jsSum callWithArguments:[NSArray arrayWithObjects:@12, @33, nil]];

如同这个例子,我们可以方便的通过JSValue对象的 callWithArguments:方法来直接调取 js 的 function。js function的多参数,在OC中,由NSArray组装而成。

###通过js执行OC方法或调取OC属性。

####有两种方式可以方便的通过js 调用 OC:

  1. Block 用来调用方法。
    我们有一个OC方法,提供给js调用
    1
    2
    3
    4
    - (NSInteger)sumWithA:(NSInteger)a B:(NSInteger)b C:(NSInteger)c
    {
    return a + b + c;
    }
  • (void)jsToOcFunction
    {
    //需要写的代码
    JSContext *context = [[JSContext alloc] init];
    context[@”sumNums”] = ^(NSInteger a, NSInteger b, NSInteger c) {
    return [self sumWithA:a B:b C:c];
    
    };
    //模拟执行js代码
    JSValue *sum = [context evaluateScript:@”sumNums(7, 56, 22)”];
    NSLog(@”sum %@”, sum);//sum 85
    }
    1
    2
    3
    4
    Block方式需要特别注意的是:
    1. 不论在任何情况下,不要在Block中直接使用外面的JSValue对象, 而应该把JSValue当做参数来传进Block中。
    2. 不论在任何情况下,不要在Block中直接使用外面的JSContext对象, 而应该使用 [JSContext currentContext]获取。
  1. JSExport protocol 用来调用对象。
    使用rumtime 为一个系统控件UIButton增加JSExport protocol
    1
    @protocol UIButtonExport <JSExport>
  • (void)setTitle:(NSString *)title forState:(UIControlState)state;
    @end

  • (void)changeTitle
    {
    class_addProtocol([UIButton class], @protocol(UIButtonExport));

    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    [button setTitle:@”你好 OC” forState:UIControlStateNormal];
    button.frame = CGRectMake(100, 100, 100, 100);
    [self.view addSubview:button];

    JSContext *context = [[JSContext alloc] init];
    context[@”button”] = button;
    [context evaluateScript:@”button.setTitleForState(‘你好 js’, 0)”];
    }

    1
    通过runtime的方式增加JSExport protocol之外,还可以通过category的方式,比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
//UIButton+js.h
#import <UIKit/UIKit.h>
#import <JavaScriptCore/JavaScriptCore.h>
@protocol UIButtonExport <JSExport>
@property (nonatomic,assign) int index;
- (void)setTitle:(NSString *)title forState:(UIControlState)state;
@end
@interface UIButton (js) <UIButtonExport>
@property (nonatomic,assign) int index;
- (void)setTitle:(NSString *)title forState:(UIControlState)state;
@end

可以看到,如果想要在js中调用OC 的类或者对象的方法,需要将方法在JSExport protocol中声明。

当然上面这些只是一些演练,我们在实际操作中会有些不同。

javascriptCore 实战

在最近的斑马王国2.0的app中,支付中有优惠券的使用,而优惠券是放在H5进行实现的,有个简单的OC与js的交互。
当用户点击app中的优惠券时,打开优惠券的网页,在优惠券页面中点击对应的优惠券,则将对应的优惠券选中,传回app并跳转回支付页面。

在这次的开发中,我使用了block的方式进行了实现。实现过程中,我发现JSContext的这个上下文似乎应该从页面获取,经过打印webView的相关信息,发现我们可以通过这样的方式获取javascriptContext:
[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

下面是在优惠券的h5页面声明OC方法的代码:

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
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
NSLog(@"加载成功");
if (_jsContext == nil) {
// 1.
_jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
// 2. 关联打印异常
_jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
context.exception = exceptionValue;
DDLogVerbose(@"异常信息:%@", exceptionValue);
};
//声明js调用的OC方法
__weak ZBWebViewController *weakSelf = self;
//选择优惠券代码
_jsContext[@"selectCoupons"] = ^(NSString *couponsName, NSString *couponsCode, int selected) {
return [weakSelf selectCoupons:couponsName couponsCode:couponsCode selected:selected];
};
//跳转立即体验代码
_jsContext[@"gotoExperience"] = ^(){
return [weakSelf gotoExperience];
};
}
}

我在网页加载成功的时候,来获取这个准确的JSContext,同时声明了我们将要在JS中调用的OC方法,并进行了异常信息的打印。

下面是OC中执行的优惠券选择代码:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 选择优惠券
*
* @param couponsName 优惠券名称
* @param excCode 优惠券码
* @param selected 是否选中优惠券
*/
- (void)selectCoupons:(NSString *)couponsName couponsCode:(NSString *)excCode selected:(int)selected
{
NSLog(@"-- 选择优惠券 ---couponsName:%@---disCode:%@---selected:%d",couponsName,excCode,selected);
\\执行跳转并传值。
}

而在JS中,我们只需要在选中优惠券的时候,selectCoupons(couponsName,excCode,select);这样调用即可。

##总结
通过javascriptCore,我们可以随意的在JS与OC中进行切换。
1、在OC/swift里,所有JavaScript代码都可以在JavaScript运行环境(JSContext)中通过evaluateScript运行;

2、在OC/swift里,所有JavaScript中的方法、对象、属性都可以通过objectForKeyedSubscript(类似字典的方式,context.objectForKeyedSubscript("Person") == context[@"Person"],上面的演示都用的是后者)来取得,取得所有对象均为JSValue类型

3、通过objectForKeyedSubscript取得的JavaScript中的对象,都遵循该对象在JavaScript中有的所有特性,如数组的长度,无数组越界,自动延展的特性

4、通过objectForKeyedSubscript取得的JavaScript中的方法,均可以通过callWithArguments传入参数调用JavaScript中的方法并返回正确的结果(类型仍然为JSValue,可以通过toObject的方式转化为对应的OC对象)

5、补充一点:除了通过objectForKeyedSubscript取得JavaScript对象外,我们也可以通过 setObjectForKeyedSubscript的方式给JavaScript中传递类型或对象(传递类型:context[@”Book”] = [Book class];传递对象:context[@”book”] = book),传递完成的对象或者类型,可以在JavaScript中直接使用,类似button.setTitleForState('你好 js', 0)

6、我们可以通过继承JSExport的中自定义协议(@protocol)(定义的属性、方法)的方式,让给任意类型添加可以在JavaScript中访问到的属性、方法,方便JS直接进行点操作


本作品采用 署名-非商业性使用-相同方式共享 2.5 中国大陆 (CC BY-NC-SA 2.5)协议 进行许可,欢迎转载,但转载请注明来自SarielTang,并保持转载后文章内容的完整。本人保留所有版权相关权利。

本文永久链接:http://sarieltang.github.io/2015/12/25/总结分析/2015-12-25/index/