初识JavaScriptCore

JavaScriptCore介绍

  • OS X Mavericks 和 iOS 7 引入了 JavaScriptCore 库,它把 WebKit 的 JavaScript 引擎用 Objective-C 封装,提供了简单,快速以及安全的方式接入世界上最流行的语言。不管你爱它还是恨它,JavaScript 的普遍存在使得程序员、工具以及融合到 OS X 和 iOS 里这样超快的虚拟机中资源的使用都大幅增长.

  • 在之前的版本,你只能通过向UIWebView发送stringByEvaluatingJavaScriptFromString:消息来执行一段JavaScript脚本,并且如果想用JavaScript调用OC,必须打开一个自定义的URL(例如axe://),然后在webView:shouldStartLoadWithRequest:navigationType:中处理.

  • JavaScriptCore的先进功能

    • 运行JavaScript脚本而不需要依赖UIWebView
    • 使用现代Objective-C的语法(例如Blocks和下标)
    • 在Objective-C和JavaScript之间无缝的传递值或者对象
    • 创建混合对象(原生对象可以将JavaScript值或函数作为一个属性)

JavaScriptCore概述

  • JSValue:代表一个JavaScript实体,一个JSValue可以表示很多JavaScript原始类型例如boolean, integers, doubles,甚至包括对象和函数.
  • JSManagedValue:本质上是一个JSValue,但是可以处理内存管理中的一些特殊情形,它能帮助引用计数和垃圾回收这两种内存管理机制之间进行正常的切换.
  • JSContext:代表JavaScript的运行环境,你需要用JSContext来执行JavaScript代码,所有的JSValue都是捆绑在一个JSContext上的.
  • JSExport:这是一个协议,可以用这个协议将原生对象导出给JavaScript,这样原生对象的属性或者方法就成了JavaScript的属性或者方法,很神奇!
  • JSVirtualMachine:代表一个对象空间,拥有自己的堆结构和垃圾回收机制,大部分情况下不需要和它直接交互,除非要处理一些特殊的多线程或者内存管理问题.

JSContext/JSValue

  • JSContext是运行JavaScript代码的环境,一个JSContext是一个全局环境的实例,如果你写过一个在浏览器内运行的JavaScript, JSContext类似于window,创建一个JSContext后,可以很容易的运行JavaScript代码来创建变量,做计算, 甚至定义方法:
    JSContext *context = [[JSContext alloc] init];
    [context evaluateScript:@"var num = 1 + 2"];
    [context evaluateScript:@"var names = ['Giant', 'Axe', 'GA']"];
    [context evaluateScript:@"var triple = function(value) { return value * 3 }"];
    JSValue *tripleNum = [context evaluateScript:@"triple(num)"];
  • 代码的最后一行,任何出自JSContext的值都被包裹在一个JSValue对象中,像JavaScript这样的动态语言需要一个动态类型,所以JSValue包装了每一个可能的JavaScript值:字符串;数字;数组;对象;方法;甚至错误和特殊的JavaScript值比如null和undefined.
  • JSValue包括了一系列方法用于访问其可能的值以保证有正确的Foundation类型,包括如下:
JavaScript Type JSValue Method Objective-C Type Swift Type
string toString NSString String!
boolean toBool BOOL Bool
number toNumber,toDouble,toInt32,toUInt32 NSNumber,double,int32_t,uint32_t NSNumber!,Double,Int32,UInt32
Date toDate NSDate NSDate!
Array toArray NSArray [AnyObject]!
Object toDictionary NSDictionary [NSObject : AnyObject]!
Object toObject,toObjectOfClass: custom type custom type
  • 从上面的例子中得到tripleNum的值,只需使用适当的方法:
NSLog(@"Tripled: %d", [tripleNum toInt32]);
// Tripled: 9

下标值

  • 对JSContext和JSValue实例使用下标的方式,我们可以很容易的访问我们之前创建的context的任何值. JSContext需要一个字符串下标,而JSValue允许使用字符串或整数标来得到里面的对象和数组:
JSValue *names = context[@"names"];
JSValue *initialName = names[0];
NSLog(@"The first name: %@", [initialName toString]);
// The first name: Giant

调用方法

  • JSValue包装了一个JavaScript函数,我们可以从OC代码中使用Foundation类型作为参数直接调用该函数:
JSValue *tripleFunction = context[@"triple"];
JSValue *result = [tripleFunction callWithArguments:@[@5]];
NSLog(@"Five tripled: %d", [result toInt32]);

错误处理

  • JSContext还有另外一个有用的招数,通过设置上下文的exceptionHandler属性,你可以观察和记录语法,类型以及运行时错误,exceptionHandler是一个接收一个JSContext引用和异常本身的回调处理:
context.exceptionHandler = ^(JSContext *context, JSValue *exception) { 
  NSLog(@"JS Error: %@", exception);
};
[context evaluateScript:@"function multiply(value1, value2) { return value1 * value2 "];
// JS Error: SyntaxError: Unexpected end of script

JavaScript调用

oc调js

  • 例如有一个"Hello.js"文件内容如下:
function printHello() {
}
  • 在Objective-C中调用printHello方法:
    // 取出js路径
    NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"hello" ofType:@"js"];
    
    // UTF8编码
    NSString *scriptString = [NSString stringWithContentsOfFile:scriptPath encoding:NSUTF8StringEncoding error:nil];
    
    // 初始化JSContext
    JSContext *context = [[JSContext alloc] init];
    
    // 执行JavaScript脚本
    [context evaluateScript:scriptString];
    
    // 取出printHello函数,保存到JSValue中
    JSValue *function = self.context[@"printHello"];
    
    // 调用(如果JSValue是一个js函数,可以用callWithArguments来调用,参数是一个数组,如果没有参数则传入空数组@[])
    [function callWithArguments:@[]];

js调oc

JS调用OC有两个方法:block和JSExport protocol。
  • Block方法:
    // 初始化JSContext
    self.context = [[JSContext alloc] init];
    
    // 定义block保存到context中
    self.context[@"add"] = ^(NSInteger a, NSInteger b) {
        NSLog(@"addNum : %@", @(a + b));
    };
    
    // 执行javaScript
    [self.context evaluateScript:@"add(2,3)"];
  • ** JSExport**方法:
    • 新建一个类,遵守一个我们自定义的继承自JSExport的协议:
    • 然后我们在VC里测试.
#import <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>

@protocol JSTestDelegate <JSExport>

// 测试无参数
- (void)testNoPara;
// 测试一个参数
- (void)testOnePara:(NSString *)msg;
// 测试两个参数
- (void)testTwoPara:(NSString *)msg1 secondPara:(NSString *)msg2;

@end

@interface testJSObject : NSObject <JSTestDelegate>

@end
#import "testJSObject.h"

@implementation testJSObject

- (void)testNoPara
{
    NSLog(@"no para");
}

- (void)testOnePara:(NSString *)msg
{
    NSLog(@"one para");
}

- (void)testTwoPara:(NSString *)msg1 secondPara:(NSString *)msg2
{
    NSLog(@"two para");
}

@end

    // 创建JSContext
    self.context = [[JSContext alloc] init];

    //设置异常处理 
    self.context.exceptionHandler = ^(JSContext *context,JSValue *exception) {
        [JSContext currentContext].exception = exception; 
        NSLog(@"exception:%@",exception);                 
    };
    
    // 将testObj添加到context中
    testJSObject *testObj = [testJSObject new];
    self.context[@"testObject"] = testObj;
   
    NSString *jsStr1 = @"testObject.testNoPara()";
    NSString *jsStr2 = @"testObject.testOnePara()";
    [self.context evaluateScript:jsStr1];
    [self.context evaluateScript:jsStr2];
  • demo比较简单,控制台输出结果如下:
打印结果
  • 唯一要注意的是OC的函数命名和JS函数命名规则问题,协议中定义的testNoPara,testOnePara:,testTwoPara:secondPara:js调用时要注意.

内存管理陷阱

  • Objective-C的内存管理机制是引用计数,JavaScript的内存管理机制是垃圾回收。在大部分情况下,JavaScriptCore能做到在这两种内存管理机制之间无缝无错转换,但也有少数情况需要特别注意.

在block内捕获JSContext

  • Block会为默认为所有被它捕获的对象创建一个强引用。JSContext为它管理的所有JSValue也都拥有一个强引用。并且,JSValue会为它保存的值和它所在的Context都维持一个强引用。这样JSContext和JSValue看上去是循环引用的,然而并不会,垃圾回收机制会打破这个循环引用。看如下例子:
    self.context[@"getVersion"] = ^{
        NSString *versionString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
        
        versionString = [@"version " stringByAppendingString:versionString];
        
        JSContext *context = [JSContext currentContext]; // 这里不要用self.context
        JSValue *version = [JSValue valueWithObject:versionString inContext:context];
        
        return version;
    };
  • 使用[JSContext currentContext]而不是self.context来在block中使用JSContext,来防止循环引用。

JSManagedValue

  • 当把一个JavaScript值保存到一个本地实例变量上时,需要尤其注意内存管理陷阱。 用实例变量保存一个JSValue非常容易引起循环引用。
  • 看以下下例子,自定义一个UIAlertView,当点击按钮时调用一个JavaScript函数:
#import <UIKit/UIKit.h>
#import <JavaScriptCore/JavaScriptCore.h>

@interface MyAlertView : UIAlertView

- (id)initWithTitle:(NSString *)title
            message:(NSString *)message
            success:(JSValue *)successHandler
            failure:(JSValue *)failureHandler
            context:(JSContext *)context;

@end
  • 按照一般自定义AlertView的实现方法,MyAlertView需要持有successHandlerfailureHandle这两个JSValue对象

  • 向JavaScript环境注入一个function:

    self.context[@"presentNativeAlert"] = ^(NSString *title,
                                            NSString *message,
                                            JSValue *success,
                                            JSValue *failure) {
        JSContext *context = [JSContext currentContext];
        MyAlertView *alertView = [[MyAlertView alloc] initWithTitle:title
                                                            message:message
                                                            success:success
                                                            failure:failure
                                                            context:context];
        [alertView show];
    };
  • 因为JavaScript环境中都是“强引用”(相对Objective-C的概念来说)的,这时JSContext强引用了一个presentNativeAlert函数,这个函数中又强引用了MyAlertView 等于说JSContext强引用了MyAlertView,而MyAlertView为了持有两个回调强引用了successHandlerfailureHandler这两个JSValue,这样MyAlertView和JavaScript环境互相引用了。

  • 所以苹果提供了一个JSManagedValue类来解决这个问题。

  • 看MyAlertView.m的正确实现:

#import "MyAlertView.h"

@interface MyAlertView() <UIAlertViewDelegate>
@property (strong, nonatomic) JSContext *ctxt;
@property (strong, nonatomic) JSMagagedValue *successHandler;
@property (strong, nonatomic) JSMagagedValue *failureHandler;
@end

@implementation MyAlertView

- (id)initWithTitle:(NSString *)title
            message:(NSString *)message
            success:(JSValue *)successHandler
            failure:(JSValue *)failureHandler
            context:(JSContext *)context {
    
    self = [super initWithTitle:title
                        message:message
                       delegate:self
              cancelButtonTitle:@"No"
              otherButtonTitles:@"Yes", nil];
    
    if (self) {
        _ctxt = context;
        
        _successHandler = [JSManagedValue managedValueWithValue:successHandler];
        // A JSManagedValue by itself is a weak reference. You convert it into a conditionally retained
        // reference, by inserting it to the JSVirtualMachine using addManagedReference:withOwner:
        [context.virtualMachine addManagedReference:_successHandler withOwner:self];
        
        _failureHandler = [JSManagedValue managedValueWithValue:failureHandler];
        [context.virtualMachine addManagedReference:_failureHandler withOwner:self];
    }
    return self;
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    if (buttonIndex == self.cancelButtonIndex) {
        JSValue *function = [self.failureHandler value];
        [function callWithArguments:@[]];
    } else {
        JSValue *function = [self.successHandler value];
        [function callWithArguments:@[]];
    }
    
    [self.ctxt.virtualMachine removeManagedReference:_failureHandler withOwner:self];
    [self.ctxt.virtualMachine removeManagedReference:_successHandler withOwner:self];
}    

@end
  • 分析上面例子,从外部传入的JSValue对象在类内部使用JSManagedValue来保存.
  • JSManagedValue本身是一个弱引用对象,需要调用JSVirtualMachine的addManagedReference:withOwner:
    把它添加到JSVirtualMachine对象中,确保使用过程中JSValue不会被释放.
  • 当用户点击AlertView上的按钮时,根据用户点击哪一个按钮,来执行对应的处理函数,这时AlertView也随即被销毁.这时需要手动调用removeManagedReference:withOwner:
    来移除JSManagedValue.

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容