JavaScriptCore使用

版权声明:此文章转载自infocool

原文链接:http://www.infocool.net/kb/IOS/201607/164059.html

如需转载请联系听云College团队成员小尹 邮箱:yinhy#tingyun.comiOS7以前我们对JS的操作只有webview里面一个函数 stringByEvaluatingJavaScriptFromString,JS对OC的回调都是基于URL的拦截进行的操作。 

iOS7后苹果在iPhone平台推出JavaScriptCore,极大的方便了我们对js的操作,可以脱离webview直接运行我们的js,使我们可以在js与oc直接切换执行代码。 

JavaScriptCore是webkit的一个重要组成部分,主要是对JS进行解析和提供执行环境。

常用介绍:

JSContext :JS执行的环境,同时也通过JSVirtualMachine管理着所有对象的生命周期,每个JSValue都和JSContext相关联并且强引用context。 

JSValue:JS对象在JSVirtualMachine中的一个强引用,其实就是Hybird对象。我们对JS的操作都是通过它。并且每个JSValue都是强引用一个context。同时,OC和JS对象之间的转换也是通过它。 

JSVirtualMachine:JS运行的虚拟机,有独立的堆空间和垃圾回收机制。为JavaScript的运行提供了底层资源,JSContext就为其提供着运行环境。 

JSManagedValue:JS和OC对象的内存管理辅助对象。由于JS内存管理是垃圾回收,并且JS中的对象都是强引用,而OC是引用计数。如果双方相互引用,势必会造成循环引用,而导致内存泄露。我们可以用JSManagedValue保存JSValue来避免。 

JSExport:一个协议,如果JS对象想直接调用OC对象里面的方法和属性,那么这个OC对象只要实现这个JSExport协议就可以了。

一、基本类型转换

oc与js对象类型的对应关系:

 Objective-C type  |   JavaScript type  
 --------------------+---------------------
         nil         |     undefined
        NSNull       |        null
       NSString      |       string
       NSNumber      |   number, boolean
     NSDictionary    |   Object object
       NSArray       |    Array object
        NSDate       |     Date object
       NSBlock (1)   |   Function object (1)
          id (2)     |   Wrapper object (2)
        Class (3)    | Constructor object (3)

oc是强类型的语言,而js是弱类型的,所以,oc中保存js的值是由JSValue来处理的。 

oc与js对象之间类型的转换通过JSValue来对接和转化,如下代码

JSContext *context = [[JSContext alloc] init];
    JSValue *jsVal = [context evaluateScript:@"21+7"]; int iVal = [jsVal toInt32]; NSLog(@"JSValue: %@, int: %d", jsVal, iVal);

通过JSContext的evaluateScript执行一段js脚本,该脚本返回了计算结果,并保存在JSValue中。通过[jsVal toInt32]将类型转化为其实实际类型int。

二、变量对象互相调用

JSContext *context = [[JSContext alloc] init];
    [context evaluateScript:@"var arr = [21, 7 , 'iderzheng.com'];"];
    JSValue *jsArr = context[@"arr"]; // Get array from JSContext
    NSLog(@"JS Array: %@; Length: %@", jsArr, jsArr[@"length"]);
    jsArr[1] = @"blog"; // Use JSValue as array
    jsArr[7] = @7;
    NSLog(@"JS Array: %@; Length: %d", jsArr, [jsArr[@"length"] toInt32]);
    NSArray *nsArr = [jsArr toArray];
    NSLog(@"NSArray: %@", nsArr);

通过js定义并赋值了数字变量arr,在oc中通过context[@”变量名”]的方式获取到js中的变量,在oc中以JSValue保存,这里就是用context[@”arr”]获取。

三、方法的互相转换调用

1.使用block定义js方法,使用js脚本来调用

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 });"];

当执行js脚本调用log方法时,会最终执行block方法。

2.在js中定义方法,在oc中调用

JSContext *context = [[JSContext alloc] init];
    [context evaluateScript:@"function add(a, b) { return a + b; }"];
    JSValue *add = context[@"add"];
    NSLog(@"Func: %@", add);
    //相当于 js方法中的this,在一个js全局函数中的this为globalObject(在浏览器中的window),在js函数外调用返回nil
    JSValue *this=[JSContext currentThis];
    JSValue *globalObject=[context globalObject]; //因为add是全局函数
    NSLog(@"this:%@,globalObject:%@",this,globalObject);
    JSValue *sum1=[globalObject invokeMethod:@"add" withArguments:@[@(11), @(21)]];
    NSLog(@"sum1: %d",[sum1 toInt32]);
    JSValue *sum = [add callWithArguments:@[@(7), @(21)]];
    NSLog(@"Sum: %d",[sum toInt32]);

代码中执行js脚本定义了js方法function add(a, b),在oc中通过context[@”方法名”]的方式引用js方法,这里就是context[@”add”],最后通过JSValue方法callWithArguments调用方法并传入参数。 

这里还可以用另外一种调用js方法的方式,因为add在js中是全局函数,可以通过获得globalObject来调用其范围中的方法,然后使用invokeMethod执行js方法。

四、异常处理

在oc中执行js的时候,如何捕获js抛出的异常呢,这里可通过设置JSContext的exceptionHandler属性,使其能够捕获在context中js脚本的报错。如下:

JSContext *context = [[JSContext alloc] init];
    context.exceptionHandler = ^(JSContext *con, JSValue *exception) {
        NSLog(@"%@", exception);
        con.exception = exception;
    };
    [context evaluateScript:@"var ider.zheng = function(){return 1};"];

PS:使用Block的注意事项 

无论是把Block传给JSContext对象让其变成JavaScript方法,还是把它赋给exceptionHandler属性,在Block内都不要直接使用其外部定义的JSContext对象或者JSValue,应该将其当做参数传入到Block中,或者通过JSContext的类方法+ (JSContext *)currentContext;来获得。否则会造成循环引用使得内存无法被正确释放。

五、oc与js 复杂的类型 互调

1.键值对编程—Dictionary

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

//js -> oc
[context evaluateScript:@"var jsObj = { number:7, name:'Ider' };"];
    JSValue *obj=context[@"jsObj"];
    NSLog(@"%@, %@", obj[@"name"], obj[@"number"]);
    NSDictionary *dic_oc = [obj toDictionary];
    NSLog(@"%@, %@", dic_oc[@"name"], dic_oc[@"number"]);
    //oc->js
    NSDictionary *dic = @{@"name": @"I am ocDic", @"ocNumber":@(21)};
    context[@"dic"] = dic;
    [context evaluateScript:@"log(dic.name, dic['ocNumber'])"];

2.自定义对象互调

对于自定义的对象,如果要让其在js中能够范围其属性和方法,就需要在两者之间建立一个映射关系。 

这里 语言穿梭机—JSExport协议 发挥作用。 

JSExport中没有约定任何的方法,连可选的(@optional)都没有,但是所有继承了该协议(@protocol)的协议中定义的方法,都可以在JSContext中被使用。 

比如,设有一个Person类,其拥有属性和方法

@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
- (void)setFirstName:(NSString *)first andLastName:(NSString *)last;

为了能够实现互调,我们为其定义一个协议,如下

protocol PersonProtocol <JSExport>
@property (nonatomic, retain) NSDictionary *urls;
- (NSString *)fullName;
//setFirstNameAndLastName('xiao','lu')
- (void)setFirstName:(NSString *)first andLastName:(NSString *)last;
@end

然后Person遵循该协议

@interface Person : NSObject<PersonProtocol>

实现协议

@implementation Person
@synthesize firstName, lastName, urls;
- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
- (void)setFirstName:(NSString *)first andLastName:(NSString *)last{
    self.firstName=first;
    self.lastName=last;
}
@end

下面就可以正常使用了

Person *person = [[Person alloc] init];
    context[@"p"] = person;
    person.firstName = @"Ider";
    person.lastName = @"Zheng";
    person.urls = @{@"site": @"http://www.iderzheng.com"};
    //得到undefined 只有在协议中的属性,才会得到反映,才能是js调用到
    [context evaluateScript:@"log(p.firstName);"];
    //fullName方法能打印,但这并不影响从fullName()中正确得到未定义协议的两个属性的值
    [context evaluateScript:@"log(p.fullName());"];
    // ok to access dictionary as object
    [context evaluateScript:@"log('site:', p.urls.site, 'blog:', p.urls.blog);"];
    // ok to change urls property
    [context evaluateScript:@"p.urls = {blog:'http://blog.iderzheng.com'}"];//在js中修改对象值
    [context evaluateScript:@"log('-------AFTER CHANGE URLS-------')"];
    [context evaluateScript:@"log('site:', p.urls.site, 'blog:', p.urls.blog);"];
    // oc对象也会被修改
    NSLog(@"%@", person.urls);
    //调用oc方法
    /*对于多参数的方法,JavaScriptCore的转换方式将Objective-C的方法每个部分都合并在一起,冒号后的字母变为大写并移除冒号*/
    [context evaluateScript:@"p.setFirstNameAndLastName('xiao','lu');"];
    NSLog(@"%@,%@", person.firstName,person.lastName);

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

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

对于多参数的方法,JavaScriptCore的转换方式将Objective-C的方法每个部分都合并在一起,冒号后的字母变为大写并移除冒号,如- (void)setFirstName:(NSString *)first andLastName:(NSString *)last在js中调用就是p.setFirstNameAndLastName('xiao','lu')

JSExportAs 定义别名 

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

@protocol PersonProtocol <JSExport>

//JSExportAs 使用一个方法别名来定义方法,可避免方法转化为js函数后方法名过长

JSExportAs(shortFuntion,
           - (void)setFirst:(NSString *)first andLast:(NSString *)last
           );
@end

然后,Person类声明和定义方法

- (void)setFirst:(NSString *)first andLast:(NSString *)last;
 (void)setFirst:(NSString *)first andLast:(NSString *)last{
    self.firstName=first;
    self.lastName=last;
}

之后你就可以直接用别名在js中调用方法了

//JSExportAs 调用js方法别名

    [context evaluateScript:@"p.shortFuntion('cao','short');"];
    NSLog(@"%@,%@", person.firstName,person.lastName);

3.对已定义类扩展协议— class_addProtocol

对于已经定义好的系统类或者从外部引入的库类,她们都不会预先定义协议提供与JavaScript的交互的,所以这里只能使用oc的runtime机制来修改它们了。 

比如下边的例子,就是为UILabel添加了协议,让其能在JavaScript中可以直接访问text属性。该接口如下

@protocol JSUILabelExport <JSExport>
@property(nonatomic,copy) NSString *text;
@end

之后在通过class_addProtocol为其添加上该协议,

- (void)viewDidLoad {
    [super viewDidLoad];
    //给系统类添加js协议,令js能够访问其text属性
    class_addProtocol([UILabel class], @protocol(JSUILabelExport));
}

然后就可以正常互调了,

JSContext *context = [[JSContext alloc] init];
    context[@"testLabel"] = _testLabel;
    NSString *script = @"var num = parseInt(testLabel.text, 10);"
    "++num;"
    "testLabel.text = num;";
    [context evaluateScript:script];

从结果发现,通过js脚本,修改到了UILabel的text了。

六、内存管理

现在来说说内存管理的注意点,OC使用的ARC,JS使用的是垃圾回收机制,并且所有的引用是都强引用,不过JS的循环引用,垃圾回收会帮它们打破。JavaScriptCore里面提供的API,正常情况下,OC和JS对象之间内存管理都无需我们去关心。不过当两种不同的内存回收机制在同一个程序中被使用时就难免会产生冲突。

比如,在一个方法中创建了一个临时的Objective-C对象,然后将其加入到JSContext放在JavaScript中的变量中被使用。因为JavaScript中的变量有引用所以不会被释放回收,但是Objective-C上的对象可能在方法调用结束后,引用计数变0而被回收内存,因此JavaScript层面也会造成错误访问。 

同样的,如果用JSContext创建了对象或者数组,返回JSValue到Objective-C,即使把JSValue变量retain下,但可能因为JavaScript中因为变量没有了引用而被释放内存,那么对应的JSValue也没有用了。

怎么在两种内存回收机制中处理好对象内存就成了问题。JavaScriptCore提供了JSManagedValue类型帮助开发人员更好地管理对象内存。

JSManagedValue 帮助我们保存JSValue,那里面保存的JS对象必须在JS中存在,同时 JSManagedValue 的owner在OC中也存在。我们可以通过它提供的两个方法+ (JSManagedValue )managedValueWithValue:(JSValue )value; + (JSManagedValue )managedValueWithValue:(JSValue )value andOwner:(id)owner 创建 JSManagedValue对象。通过JSVirtualMachine 的方法 - (void)addManagedReference:(id)object withOwner:(id)owner 来建立这个弱引用关系。通过 - (void)removeManagedReference:(id)object withOwner:(id)owner 来手动移除他们之间的联系。

如打破循环,就需要在oc上对需要保存的js对象封装为JSManagedValue,并把JSManagedValue交给JSVirtualMachine来管理:

 _managedValue = [JSManagedValue managedValueWithValue:jsObject]; [[[JSContext currentContext] virtualMachine] addManagedReference:_managedValue withOwner:self];

内存管理总结: 

1、不要在block里面直接使用context,或者使用外部的JSValue对象。 

2、OC对象不要用属性直接保存JSValue对象,因为这样太容易循环引用了。

1.png

想阅读更多技术文章,请访问听云技术博客,访问听云官方网站感受更多应用性能优化魔力。

关于作者

郝淼emily

重新开始,从心开始

我要评论

评论请先登录,或注册