UIWebView---JavaScriptCore框架介绍

References:

  1. Steamclock Software – Apple’s new Objective-C to Javascript Bridge
  2. JavaScriptCore and iOS 7 » Big Nerd Ranch BlogBig Nerd Ranch Blog
  3. IOS7开发~JavaScriptCore (二) – 阿福的专栏 – 博客频道 – CSDN.NET
  4. API in trunk/Source/JavaScriptCore – WebKit
  5. Objective-C Runtime Reference
  6. Automatic Reference Counting vs. Garbage Collection – The Oxygene Language Wiki
  7. iOS7新JavaScriptCore框架介绍

JavaScriptCore介绍

基本概念


JavaScriptCore.framework :iOS7 中新加入的框架,用来处理JavaScript。JavaScriptCore 是苹果 Safari 浏览器的 JavaScript 引擎,JavaScriptCor在 OS X 平台上很早就存在的,而在 iOS 平台,直到IOS7才对外开放,并提供了 Objective-C 的接口

这个框架其实只是基于webkit中以C/C++实现的JavaScriptCore的一个包装,在旧版本iOS开发中,很多开发者也会自行将webkit的库引入项目编译使用。不过虽然iOS7把它当成了标准库,可惜目前,我还没有在Apple Developer中找到像那样的官方文档介绍这个框架的具体使用方法。

JavaScriptCore中的类

在项目中引入JavaScriptCore后,链到头文件中,除了大段的Copyright注释可以看到里面只要引入了5个文件,每个文件里都定义跟文件名对应的类:

  • JSContext
  • JSValue
  • JSManagedValue
  • JSVirtualMachine
  • JSExport

JavaScriptCore_Head

虽说代码中的注释介绍的也比较详细了,但是如同一图顶万言,对程序员来说代码更有说服力。本文就先来说说这些类相对比较好理解但又很常用的JSContext和JSValue以及它们方法的使用方式和效果。

JSContext和JSValue

JSVirtualMachine为JavaScript的运行提供了底层资源,JSContext就为其提供着运行环境,通过- (JSValue *)evaluateScript:(NSString *)script;方法就可以执行一段JavaScript脚本,并且如果其中有方法、变量等信息都会被存储在其中以便在需要的时候使用

JSContext:An instance of JSContext represents a JavaScript execution environment.(一个 Context 就是一个 JavaScript 代码执行的环境,也叫作用域。),

而JSContext的创建都是基于JSVirtualMachine- (id)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;,如果是使用- (id)init;进行初始化,那么在其内部会自动创建一个新的JSVirtualMachine对象然后调用前边的初始化方法。

JSValue:Conversion between Objective-C and JavaScript types.(JS是弱类型的,ObjectiveC是强类型的,JSValue被引入处理这种类型差异,在 Objective-C 对象和 JavaScript 对象之间起转换作用),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    



首先引入框架   #import <JavaScriptCore/JavaScriptCore.h>

Objective-C中调用JavaScript

基本类型转换(oc调js)

先看个简单的例子:
    JSContext *context = [[JSContext alloc] init];
    JSValue *jsVal = [context evaluateScript:@"21+7"];
    int iVal = [jsVal toInt32];
    NSLog(@"JSValue: %@, int: %d", jsVal, iVal);
 
 很简单吧,还可以存一个JavaScript变量在 
 JSContext 
 中,然后通过下标来获取出来。而对于 
 Array 
 或者 
 Object 
 类型, 
 JSValue 
 也可以通过下标直接取值和赋值。 
     JSContext *context = [[JSContext alloc] init];
     [context evaluateScript:@"var arr = [21, 7 , 'iderzheng.com'];"];
     JSValue *jsArr = context[@"arr"]; // Get array from JSContext
     
     NSLog(@"JS Array1: %@; Length: %@", jsArr, jsArr[@"length"]);
     jsArr[1] = @"blog"; // Use JSValue as array
     jsArr[7] = @7;
     
     NSLog(@"JS Array2: %@; Length: %d", jsArr, [jsArr[@"length"] toInt32]);
     
     NSArray *nsArr = [jsArr toArray];
     NSLog(@"NSArray: %@", nsArr);
     // JS Array: 21,blog,iderzheng.com,,,,,7 Length: 8
     // NSArray: (
     // 21,
     // blog,
     // "iderzheng.com",
     // "<null>",
     // "<null>",
     // "<null>",
     // "<null>",
     // 7
     // )

 
 通过输出结果很容易看出代码成功把数据从Objective-C赋到了JavaScript数组上,而且 
 JSValue 
 是遵循JavaScript的数组特性:无下标越位,自动延展数组大小。并且通过 
 JSValue 
 还可以获取JavaScript对象上的属性,比如例子中通过 
 "length" 
 就获取到了JavaScript数组的长度。在转成 
 NSArray 
 的时候,所有的信息也都正确转换了过去。 

方法转换(oc调js)

JSValue提供了- (JSValue *)callWithArguments:(NSArray *)arguments;方法可以反过来将参数传进去来调用方法。
    JSContext *context = [[JSContext alloc] init];
    [context evaluateScript:@"function add(a, b) { return a + b; }"];
    JSValue *add = context[@"add"];
    NSLog(@"Func: %@", add);
    
    JSValue *sum = [add callWithArguments:@[@(7), @(21)]];
    NSLog(@"Sum: %d",[sum toInt32]);
JSValue 还提供 - (JSValue *)invokeMethod:(NSString *)method withArguments:(NSArray *)arguments; 让我们可以直接简单地调用对象上的方法。只是如果定义的方法是全局函数,那么很显然应该在 JSContext globalObject 对象上调用该方法;如果是某JavaScript对象上的方法,就应该用相应的 JSValue 对象调用。
    JSContext *context = [[JSContext alloc] init];
    [context evaluateScript:@"function add(a, b) { return a + b; }"];
    JSValue *sum = [context.globalObject invokeMethod:@"add" withArguments:@[@(7), @(21)]];
    NSLog(@"Sum: %d",[sum toInt32]);

JavaScript调用Objective-C

 可以通过两种方式在 JavaScript 中调用 Objective-C

■ Blocks :JS functions (对应 JS 函数
 JSExport protocol :JS objects (对应 JS 对象

 OC的NSDictionary (对应 JS 对象

Block

    JSContext *context = [[JSContext alloc] init];
    context[@"log"] = ^() {
        NSLog(@"+++++++Begin Log+++++++");
        NSArray *args = [JSContext currentArguments];
        for (JSValue *jsVal in args) {
            NSLog(@"%@", jsVal);
        }
        
        JSValue *this = [JSContext currentThis];
        NSLog(@"this: %@",this);
        NSLog(@"-------End Log-------");
    };
    
    [context evaluateScript:@"log('ider', [7, 21], { hello:'world', js:100 });"];

注意:

1、Avoid capturing JSValues,  Prefer passing as arguments.(不要在 Block 中直接使用外面的 JSValue 对象, 把 JSValue 当做参数来传进 Block 中。)

2、Avoid capturing JSContexts, Use + [JSContext currentContext] (避免循引用,不要在 Block 中直接引用使用外面的 JSContext 对象,应该用[JSContext currentContext];)

错误的做法:



正确的做法:



异常处理

Objective-C的异常会在运行时被Xcode捕获,而在JSContext中执行的JavaScript如果出现异常,只会被JSContext捕获并存储在exception属性上,而不会向外抛出。时时刻刻检查JSContext对象的exception是否不为nil显然是不合适,更合理的方式是给JSContext对象设置exceptionHandler,它接受的是^(JSContext *context, JSValue *exceptionValue)形式的Block。其默认值就是将传入的exceptionValue赋给传入的contextexception属性:

1     ^(JSContext *context, JSValue *exceptionValue) {
2         context.exception = exceptionValue;
3     };

我们也可以给exceptionHandler赋予新的Block以便在JavaScript运行发生异常的时候我们可以立即知道:

复制代码
 1     JSContext *context = [[JSContext alloc] init];
 2     context.exceptionHandler = ^(JSContext *con, JSValue *exception) {
 3         NSLog(@"%@", exception);
 4         con.exception = exception;
 5     };
 6      
 7     [context evaluateScript:@"ider.zheng = 21"];
 8      
 9     //Output:
10     // ReferenceError: Can't find variable: ider

JSExport protocol---语言穿梭机

JavaScript可以脱离prototype继承完全用JSON来定义对象,但是Objective-C编程里可不能脱离类和继承了写代码。所以JavaScriptCore就提供了JSExport作为两种语言的互通协议。JSExport中没有约定任何的方法,连可选的(@optional)都没有,但是所有继承了该协议(@protocol)的协议(注意不是Objective-C的类(@interface))中定义的方法,都可以在JSContext中被使用。语言表述起来有点绕,还是用例子来说明会更明确一点。

js调用oc数据

@protocol PersonProtocol <JSExport>
@property (nonatomic, retain) NSDictionary *urls;
- (NSString *)fullName;
@end

@interface Person :NSObject <PersonProtocol>
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@end;

@implementation Person
@synthesize firstName, lastName, urls;
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
@end<span style="color:#ff0000;">
</span>

在上边的代码中,定义了一个PersonProtocol,并让它继承了神秘的JSExport协议,在新定义的协议中约定urls属性和fullName方法。之后又定义了Person类,除了让它实现PersonProtocol外,还定义了firstName和lastName属性。而fullName方法返回的则是两部分名字的结合。

下边就来创建一个Person对象,然后传入到JSContext中并尝试使用JavaScript来访问和修改该对象。

JSContext *context = [[JSContext alloc] init];
    Person *person = [[Person alloc] init];
    person.firstName = @"Leon";
    person.lastName = @"Nero";
    person.urls = @{@"site": @"http://www.baidu.com"};
    context[@"person"] = person;
    // 打印的
    context[@"nativeLog"] = [[NativeObjectExport alloc] init];
    [context evaluateScript:@"person.doFooWithBar('a', 'b')"];
    [context evaluateScript:@"nativeLog.log(person.urls.site)"];// 继承自JSExporter的
    [context evaluateScript:@"={person.urls site:'http://blog.iderzheng.com'}"];// 修改不了不知道啥的
    [context evaluateScript:@"nativeLog.log(person.firstName)"];// 打印出undefine没有继承自JSExporter的

从输出结果不难看出,当访问firstNamelastName的时候给出的结果是undefined,因为它们跟JavaScript没有JSExport的联系。但这并不影响从fullName()中正确得到两个属性的值。和之前说过的一样,对于NSDictionary类型的urls,可以在JSContext中当做对象使用,而且还可以正确地给urls赋予新的值,并反映到实际的Objective-C的Person对象上。

JSExport不仅可以正确反映属性到JavaScript中,而且对属性的特性也会保证其正确,比如一个属性在协议中被声明成readonly,那么在JavaScript中也就只能读取属性值而不能赋予新的值。

JS调用oc的方法

举个log的例子

#import <Foundation/Foundation.h>
@import JavaScriptCore;
@protocol NativeObjectExport <JSExport>
-(void)log:(NSString*)string;
@end

@interface NativeObject : NSObject <NativeObjectExport>
@end

.m代码:
#import "NativeObject.h"
@implementation NativeObject

-(void)log:(NSString*)string {
    NSLog(@"js: %@", string);
}
@end

调用代码:
- (void) testLog
{
    JSContext *context = [[JSContextalloc]init];
    context[@"nativeObject"] = [[NativeObjectalloc]init];
    [context evaluateScript:@"nativeObject.log(\"Hello Javascript\")"];
}

对于多参数的方法,JavaScriptCore的转换方式将Objective-C的方法每个部分都合并在一起,冒号后的字母变为大写并移除冒号。比如下边协议中的方法,在JavaScript调用就是:doFooWithBar(foo, bar);

@protocol MultiArgs <JSExport>
- (void)doFoo:(id)foo withBar:(id)bar;
@end

如果希望方法在JavaScript中有一个比较短的名字,就需要用的JSExport.h中提供的宏:JSExportAs(PropertyName, Selector)

@protocol LongArgs <JSExport>
 
JSExportAs(testArgumentTypes,
           - (NSString *)testArgumentTypesWithInt:(int)i double:(double)d 
                    boolean:(BOOL)b string:(NSString *)s number:(NSNumber *)n 
                    array:(NSArray *)a dictionary:(NSDictionary *)o
           );
 
@end

比如上边定义的协议中的方法,在JavaScript就只要用testArgumentTypes(i, d, b, s, n, a, dic);来调用就可以了。

虽然JavaScriptCore框架还没有官方编程指南,但是在JSExport.h文件中对神秘协议的表述还是比较详细的,其中有一条是这样描述的:

By default no methods or properties of the Objective-C class will be exposed to JavaScript, however methods and properties may explicitly be exported. For each protocol that a class conforms to, if the protocol incorporates the protocol JSExport, then the protocol will be interpreted as a list of methods and properties to be exported to JavaScript.

这里面有个incorporate一词值得推敲,经过验证只有直接继承了JSExport的自定义协议(@protocol)才能在JSContext中访问到。也就是说比如有其它的协议继承了上边的PersonProtocol,其中的定义的方法并不会被引入到JSContext中。从源码中也能看出JavaScriptCore框架会通过class_copyProtocolList方法找到类所遵循的协议,然后再对每个协议通过protocol_copyProtocolList检查它是否遵循JSExport协议进而将方法反映到JavaScript之中。

对已定义类扩展协议— class_addProtocol

 #import <objc/runtime.h>

实例代码1:    JS代码设置Button 的 title

@protocol UIButtonExport <JSExport>
- (void)setTitle:(NSString *)title forState:(UIControlState)state;
@end
- (void) test
{
    class_addProtocol([UIButtonclass],@protocol(UIButtonExport));
    
UIButton *button = [UIButtonbuttonWithType:UIButtonTypeSystem];
    [button setTitle:@"Hello Objective-C"forState:UIControlStateNormal];
    button.frame = CGRectMake(20,40,280,40);

    JSContext *context = [[JSContextalloc]init];
context[@"button"] = button;
[context evaluateScript:@"button.setTitleForState('Hello JavaScript', 0)"];
    
    [self.view addSubview:button];
}


上面代码中,我们申明一个 UIButtonExport 协议,该协议继承于 JSExport,并将setTitle:forState:方法开放到该协议中(只有 JSExport 协议中的方法才能被 JavaScript 识别),然后通过运行时让 UIButton 遵循 UIButtonExport 协议。这样你就可以在 JS 中为 Button 设置 title 了,需要说明一点的是,在 JS 中方法的命名规则与 Objective-C 中有点不一样,如 Objective-C 中的方法-(void)setX:(id)x Y:(id)y Z:(id)z;,加入到 JSExport 协议中,在 JS 中调用就得是setXYZ(x, y, z);,当然如果你不想根据这种命名转换规则,你也可以通过 JSExport.h 中的方法来修改:


#define JSExportAs(PropertyName, Selector) \
    @optional Selector __JS_EXPORT_AS__##PropertyName:(id)argument; @required Selector
#endif

如 setX:Y:Z 方法,我们可以给他重命名,让 JS 中通过 set3D(x,y,z) 来调用

JSExportAs(set3D,
     - (void)setX:(id)x Y:(id)y Z:(id)z
);

OC的NSDictionary

JSContext并不能让Objective-C和JavaScript的对象直接转换,毕竟两者的面向对象的设计方式是不同的:前者基于class,后者基于prototype。但所有的对象其实可以视为一组键值对的集合,所以JavaScript中的对象可以返回到Objective-C中当做NSDictionary类型进行访问。

    JSContext *context = [[JSContext alloc] init];
    JSValue * obj = [context evaluateScript: @"var jsObj = {number: 7, name: 'Ider'}; jsObj"];
    NSLog (@"%@,%@", obj [@"name"], obj [@"number"]);
    NSDictionary * dic = [obj toDictionary];
    NSLog (@ "%@,%@", dic [@"name"], dic [@"number"]);
    context[@"log"] = ^() {
        NSLog(@"+++++++Begin Log+++++++");
        NSArray *args = [JSContext currentArguments];
        for (JSValue *jsVal in args) {
            NSLog(@"%@", jsVal);
        }
    };
    NSDictionary *dic1 = @{@"name":@"IDea", @"#":@(32)};
    context[@"dic1"] = dic1;
    [context evaluateScript:@"log(dic1.name, dic1['#'])"];


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值