前阵子,Apple正式发布了新的iOS 7系统,最大最直观的改变在于界面变得小清新范了,我也提到《iOS,你真的越来越像Android了》。不过对于移动开发者来说,除了要适应Xcode 5,最应该关注的还是iOS 7在开发接口的上的变化。概览Apple提供的官方文档《What’s New in iOS》,最最让我欣喜的是iOS 7中加入了JavaScriptCore框架。该框架让Objective-C和JavaScript代码直接的交互变得更加的简单方便。
这个框架其实只是基于webkit中以C/C++实现的JavaScriptCore的一个包装,在旧版本iOS开发中,很多开发者也会自行将webkit的库引入项目编译使用。不过虽然iOS7把它当成了标准库,可惜目前,我还没有在Apple Developer中找到像之前文章中收集的那样的官方文档介绍这个框架的具体使用方法。
好在还可以在Xcode中找到头文件,而且里面的注释对每个类和方法的功能写得还算清楚,再结合网上仅有的几篇文章的介绍,我也在此简单入门一下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 |
基本类型转换
先看个简单的例子:
-
JSContext
*context
=
[
[JSContext alloc
] init
];
-
JSValue
*jsVal
=
[context evaluateScript
:
@
"21+7"
];
-
int iVal
=
[jsVal toInt32
];
-
NSLog
(
@
"JSValue: %@, int: %d", jsVal, iVal
);
-
-
//Output:
-
// JSValue: 28, int: 28
很简单吧,还可以存一个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 Array: %@; Length: %@", jsArr, jsArr
[
@
"length"
]
);
-
jsArr
[
1
]
=
@
"blog";
// Use JSValue as array
-
jsArr
[7
]
= @
7;
-
-
NSLog
(
@
"JS Array: %@; Length: %d", jsArr,
[jsArr
[
@
"length"
] toInt32
]
);
-
-
-
NSLog
(
@
"NSArray: %@", nsArr
);
-
-
//Output:
-
// JS Array: 21,7,iderzheng.com Length: 3
-
// 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
的时候,所有的信息也都正确转换了过去。
方法的转换
各种数据类型可以转换,Objective-C的Block也可以传入JSContext中当做JavaScript的方法使用。比如在前端开发中常用的log
方法,虽然JavaScritpCore没有自带(毕竟不是在网页上运行的,自然不会有window、document、console这些类了),仍然可以定义一个Block方法来调用NSLog来模拟:
-
JSContext
*context
=
[
[JSContext alloc
] init
];
-
context
[
@
"log"
]
=
^
(
)
{
-
NSLog
(
@
"+++++++Begin Log+++++++"
);
-
-
-
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 });"
];
-
-
//Output:
-
// +++++++Begin Log+++++++
-
// ider
-
// 7,21
-
// [object Object]
-
// this: [object GlobalObject]
-
// -------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;
方法可以反过来将参数传进去来调用方法。
-
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
]
);
-
//OutPut:
-
// Func: function add(a, b) { return a + b; }
-
// Sum: 28
JSValue
还提供
- (JSValue *)invokeMethod:(NSString *)method withArguments:(NSArray *)arguments;
让我们可以直接简单地调用对象上的方法。只是如果定义的方法是全局函数,那么很显然应该在
JSContext
的
globalObject
对象上调用该方法;如果是某JavaScript对象上的方法,就应该用相应的
JSValue
对象调用。
异常处理
Objective-C的异常会在运行时被Xcode捕获,而在JSContext
中执行的JavaScript如果出现异常,只会被JSContext
捕获并存储在exception
属性上,而不会向外抛出。时时刻刻检查JSContext
对象的exception
是否不为nil
显然是不合适,更合理的方式是给JSContext
对象设置exceptionHandler
,它接受的是^(JSContext *context, JSValue *exceptionValue)
形式的Block。其默认值就是将传入的exceptionValue
赋给传入的context
的exception
属性:
-
^
(JSContext
*context, JSValue
*exceptionValue
)
{
-
context.exception
= exceptionValue;
-
};
我们也可以给exceptionHandler
赋予新的Block以便在JavaScript运行发生异常的时候我们可以立即知道:
-
JSContext
*context
=
[
[JSContext alloc
] init
];
-
context.exceptionHandler
=
^
(JSContext
*con, JSValue
*exception
)
{
-
NSLog
(
@
"%@", exception
);
-
con.exception
= exception;
-
};
-
-
[context evaluateScript
:
@
"ider.zheng = 21"
];
-
-
//Output:
-
// ReferenceError: Can't find variable: ider
使用Block的注意事项
从之前的例子和介绍应该有体会到Block在JavaScriptCore中起到的强大作用,它在JavaScript和Objective-C之间的转换 建立起更多的桥梁,让互通更方便。但是要注意的是无论是把Block传给JSContext
对象让其变成JavaScript方法,还是把它赋给exceptionHandler
属性,在Block内都不要直接使用其外部定义的JSContext
对象或者JSValue
,应该将其当做参数传入到Block中,或者通过JSContext
的类方法+ (JSContext *)currentContext;
来获得。否则会造成循环引用使得内存无法被正确释放。
比如上边自定义异常处理方法,就是赋给传入JSContext
对象con
,而不是其外创建的context
对象,虽然它们其实是同一个对象。这是因为Block会对内部使用的在外部定义创建的对象做强引用,而JSContext
也会对被赋予的Block做强引用,这样它们之间就形成了循环引用(Circular Reference)使得内存无法正常释放。
对于JSValue
也不能直接从外部引用到Block中,因为每个JSValue
上都有JSContext
的引用 (@property(readonly, retain) JSContext *context;
),JSContext
再引用Block同样也会形成引用循环。