iOS开发日记28-详解JavaScriptCore
今天博主有一个OC与JS的交互的需求,遇到了一些困难点,在此和大家分享,希望能够共同进步.
上一篇博文与大家分享了如何使用webview与JS进行交互,但是随着新的API的开放,我们不仅仅需要在webview中与JS进行交互,也需要在很多其他的框架中与JS进行交互,有两种方法提供给大家:
1.苹果在7.0之后新开放得API JavaScriptCore框架,让我们能够更简便的使用OC与JS进行交互.
2.使用第三方框架WebViewJavascriptBridge
今天就和大家总结一下JavaScriptCore框架的使用.
JavaScriptCore中的类
在项目中引入JavaScriptCore后,链到头文件中,除了大段的Copyright注释可以看到里面只要引入了5个文件,每个文件里都定义跟文件名对应的类:
- JSContext
- JSValue
- JSManagedValue
- JSVirtualMachine
- JSExport
虽说代码中的注释介绍的也比较详细了,但是如同一图顶万言,对程序员来说代码更有说服力。本文就先来说说这些类相对比较好理解但又很常用的JSContext和JSValue以及它们方法的使用方式和效果。
JSContext和JSValue
JSVirtualMachine
为JavaScript的运行提供了底层资源,JSContext
就为其提供着运行环境,通过- (JSValue *)evaluateScript:(NSString *)script;
方法就可以执行一段JavaScript脚本,并且如果其中有方法、变量等信息都会被存储在其中以便在需要的时候使用。而JSContext的创建都是基于JSVirtualMachine
:- (id)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;
,如果是使用- (id)init;
进行初始化,那么在其内部会自动创建一个新的JSVirtualMachine
对象然后调用前边的初始化方法。
JSValue
则可以说是JavaScript和Object-C之间互换的桥梁,它提供了多种方法可以方便地把JavaScript数据类型转换成Objective-C,或者是转换过去。其一一对应方式可见下表:
Objective-C | JavaScript | JSValue Convert | JSValue Constructor |
---|---|---|---|
nil | undefined | valueWithUndefinedInContext | |
NSNull | null | valueWithNullInContext: | |
NSString | string | toString | |
NSNumber | number, boolean | toNumber toBool toDouble toInt32 toUInt32 |
valueWithBool:inContext: valueWithDouble:inContext: valueWithInt32:inContext: valueWithUInt32:inContext: |
NSDictionary | Object object | toDictionary | valueWithNewObjectInContext: |
NSArray | Array object | toArray | valueWithNewArrayInContext: |
NSDate | Date object | toDate | |
NSBlock | Function object | ||
id | Wrapper object | toObject toObjectOfClass: |
valueWithObject:inContext: |
Class | Constructor object |
基本类型转换
先看个简单的例子:
1 JSContext *context = [[JSContext alloc] init]; 2 JSValue *jsVal = [context evaluateScript:@"21+7"]; 3 int iVal = [jsVal toInt32]; 4 NSLog(@"JSValue: %@, int: %d", jsVal, iVal); 5 6 //Output: 7 // JSValue: 28, int: 28
很简单吧,还可以存一个JavaScript变量在JSContext
中,然后通过下标来获取出来。而对于Array
或者Object
类型,JSValue
也可以通过下标直接取值和赋值。
1 JSContext *context = [[JSContext alloc] init]; 2 [context evaluateScript:@"var arr = [21, 7 , 'iderzheng.com'];"]; 3 JSValue *jsArr = context[@"arr"]; // Get array from JSContext 4 5 NSLog(@"JS Array: %@; Length: %@", jsArr, jsArr[@"length"]); 6 jsArr[1] = @"blog"; // Use JSValue as array 7 jsArr[7] = @7; 8 9 NSLog(@"JS Array: %@; Length: %d", jsArr, [jsArr[@"length"] toInt32]); 10 11 NSArray *nsArr = [jsArr toArray]; 12 NSLog(@"NSArray: %@", nsArr); 13 14 //Output: 15 // JS Array: 21,7,iderzheng.com Length: 3 16 // JS Array: 21,blog,iderzheng.com,,,,,7 Length: 8 17 // NSArray: ( 18 // 21, 19 // blog, 20 // "iderzheng.com", 21 // "<null>", 22 // "<null>", 23 // "<null>", 24 // "<null>", 25 // 7 26 // )
通过输出结果很容易看出代码成功把数据从Objective-C赋到了JavaScript数组上,而且JSValue
是遵循JavaScript的数组特性:无下标越位,自动延展数组大小。并且通过JSValue
还可以获取JavaScript对象上的属性,比如例子中通过"length"
就获取到了JavaScript数组的长度。在转成NSArray
的时候,所有的信息也都正确转换了过去。
方法的转换
各种数据类型可以转换,Objective-C的Block也可以传入JSContext中当做JavaScript的方法使用。比如在前端开发中常用的log
方法,虽然JavaScritpCore没有自带(毕竟不是在网页上运行的,自然不会有window、document、console这些类了),仍然可以定义一个Block方法来调用NSLog来模拟:
1 JSContext *context = [[JSContext alloc] init]; 2 context[@"log"] = ^() { 3 NSLog(@"+++++++Begin Log+++++++"); 4 5 NSArray *args = [JSContext currentArguments]; 6 for (JSValue *jsVal in args) { 7 NSLog(@"%@", jsVal); 8 } 9 10 JSValue *this = [JSContext currentThis]; 11 NSLog(@"this: %@",this); 12 NSLog(@"-------End Log-------"); 13 }; 14 15 [context evaluateScript:@"log('ider', [7, 21], { hello:'world', js:100 });"]; 16 17 //Output: 18 // +++++++Begin Log+++++++ 19 // ider 20 // 7,21 21 // [object Object] 22 // this: [object GlobalObject] 23 // -------End Log-------
通过Block成功的在JavaScript调用方法回到了Objective-C,而且依然遵循JavaScript方法的各种特点,比如方法参数不固定。也因为这样,JSContext
提供了类方法来获取参数列表(+ (JSContext *)currentContext;
)和当前调用该方法的对象(+ (JSValue *)currentThis
)。对于"this"
,输出的内容是GlobalObject
,这也是JSContext
对象方法- (JSValue *)globalObject;
所返回的内容。因为我们知道在JavaScript里,所有全局变量和方法其实都是一个全局变量的属性,在浏览器中是window,在JavaScriptCore是什么就不得而知了。
Block可以传入JSContext
作方法,但是JSValue
没有toBlock
方法来把JavaScript方法变成Block在Objetive-C中使用。毕竟Block的参数个数和类型已经返回类型都是固定的。虽然不能把方法提取出来,但是JSValue
提供了- (JSValue *)callWithArguments:(NSArray *)arguments;
方法可以反过来将参数传进去来调用方法。
1 JSContext *context = [[JSContext alloc] init]; 2 [context evaluateScript:@"function add(a, b) { return a + b; }"]; 3 JSValue *add = context[@"add"]; 4 NSLog(@"Func: %@", add); 5 6 JSValue *sum = [add callWithArguments:@[@(7), @(21)]]; 7 NSLog(@"Sum: %d",[sum toInt32]); 8 //OutPut: 9 // Func: function add(a, b) { return a + b; } 10 // Sum: 28
JSValue
还提供- (JSValue *)invokeMethod:(NSString *)method withArguments:(NSArray *)arguments;
让我们可以直接简单地调用对象上的方法。只是如果定义的方法是全局函数,那么很显然应该在JSContext
的globalObject
对象上调用该方法;如果是某JavaScript对象上的方法,就应该用相应的JSValue
对象调用.
下面贴上代码,我们还是以webview作为示例,各位看官自行深究.
首先我们添加协议
@interface ViewController ()<UIWebViewDelegate>
实现代理方法
#pragma mark --webViewDelegate
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
//网页加载之前会调用此方法
//retrun YES 表示正常加载网页 返回NO 将停止网页加载
return YES;
}
-(void)webViewDidStartLoad:(UIWebView *)webView
{
//开始加载网页调用此方法
}
-(void)webViewDidFinishLoad:(UIWebView *)webView
{
//网页加载完成调用此方法
}
-(void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
//网页加载失败 调用此方法
}
每个方法是什么时候调用都在注释里面
1.我们先尝试用oc调用一下js方法
-(void)webViewDidFinishLoad:(UIWebView *)webView
{
//网页加载完成调用此方法
//首先创建JSContext 对象(此处通过当前webView的键获取到jscontext)
JSContext *context=[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
NSString *alertJS=@"alert('test js OC')"; //准备执行的js代码
[context evaluateScript:alertJS];//通过oc方法调用js的alert
}
2.下来我们使用js调用iOS
js调用iOS分两种情况
一,js里面直接调用方法
二,js里面通过对象调用方法
首先我们看第一种,直接调用方法。
其中用到了iOS的block
-(void)webViewDidFinishLoad:(UIWebView *)webView
{
//网页加载完成调用此方法
//iOS调用js
//首先创建JSContext 对象(此处通过当前webView的键获取到jscontext)
JSContext *context=[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//js调用iOS
//第一种情况
//其中test1就是js的方法名称,赋给是一个block 里面是iOS代码
//此方法最终将打印出所有接收到的参数,js参数是不固定的 我们测试一下就知道
context[@"test1"] = ^() {
NSArray *args = [JSContext currentArguments];
for (id obj in args) {
NSLog(@"%@",obj);
}
};
//此处我们没有写后台(但是前面我们已经知道iOS是可以调用js的,我们模拟一下)
//首先准备一下js代码,来调用js的函数test1 然后执行
//一个参数
NSString *jsFunctStr=@"test1('参数1')";
[context evaluateScript:jsFunctStr];
//二个参数
NSString *jsFunctStr1=@"test1('参数a','参数b')";
[context evaluateScript:jsFunctStr1];
}
以上代码中 给test1赋值的block要是执行了那么结果就是对的,js调用了iOS
我们看结果
我们看到参数被打印出来了,而且一个参数和两个参数都可以,也符合js参数个数不限制
下来我们看第二种情况 就是js 中是通过一个对象来调用方法的。
此处稍微复杂一点我们需要使用到
JSExport
凡事添加了JSExport协议的协议,所规定的方法,变量等 就会对js开放,我们可以通过js调用到
此处有点绕。我们直接看代码
首先创建一个类 继承NSObject 并且规定一个协议
#import <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>
//首先创建一个实现了JSExport协议的协议
@protocol TestJSObjectProtocol <JSExport>
//此处我们测试几种参数的情况
-(void)TestNOParameter;
-(void)TestOneParameter:(NSString *)message;
-(void)TestTowParameter:(NSString *)message1 SecondParameter:(NSString *)message2;
@end
//让我们创建的类实现上边的协议
@interface TestJSObject : NSObject<TestJSObjectProtocol>
@end
类的实现
#import "TestJSObject.h"
@implementation TestJSObject
//一下方法都是只是打了个log 等会看log 以及参数能对上就说明js调用了此处的iOS 原生方法
-(void)TestNOParameter
{
NSLog(@"this is ios TestNOParameter");
}
-(void)TestOneParameter:(NSString *)message
{
NSLog(@"this is ios TestOneParameter=%@",message);
}
-(void)TestTowParameter:(NSString *)message1 SecondParameter:(NSString *)message2
{
NSLog(@"this is ios TestTowParameter=%@ Second=%@",message1,message2);
}
@end
下面我们在weibview加载完成之后调用测试一下
-(void)webViewDidFinishLoad:(UIWebView *)webView
{
//网页加载完成调用此方法
//首先创建JSContext 对象(此处通过当前webView的键获取到jscontext)
JSContext *context=[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//第二种情况,js是通过对象调用的,我们假设js里面有一个对象 testobject 在调用方法
//首先创建我们新建类的对象,将他赋值给js的对象
TestJSObject *testJO=[TestJSObject new];
context[@"testobject"]=testJO;
//同样我们也用刚才的方式模拟一下js调用方法
NSString *jsStr1=@"testobject.TestNOParameter()";
[context evaluateScript:jsStr1];
NSString *jsStr2=@"testobject.TestOneParameter('参数1')";
[context evaluateScript:jsStr2];
NSString *jsStr3=@"testobject.TestTowParameterSecondParameter('参数A','参数B')";
[context evaluateScript:jsStr3];
}
结果如下:
TestJSOC[7038:258939] this is ios TestNOParameter
TestJSOC[7038:258939] this is ios TestOneParameter=参数1
TestJSOC[7038:258939] this is ios TestTwoParameter=参数A Second=参数B
好了 我们看到了结果 三个方法都调用了
ok两种方式都完成了
http://nshipster.cn/javascriptcore/
http://www.webryan.net/2013/10/about-ios7-javascriptcore-framework/