JavaScriptCore
其实也是一个很老的API了,之前都是在mac应用中使用,是纯c的代码,在iOS7中,用Objective-C进行了封装。JavaScriptCore
允许我们在不使用UIWebView
和WKWebView
的情况下去执行JavaScript代码。而JavaScript作为web开发的一门脚本语言,在跨平台的应用上,有着很重要的作用。许多跨平台方案,比如JSPatch,React-Native都是基于JavaScriptCore
作为iOS 端的实现方式。
JavaScriptCore
首先我们需要认识一下JavaScriptCore这个库里有什么东西。其实这个头文件只是导入了5个头文件
#import "JSContext.h"
#import "JSValue.h"
#import "JSManagedValue.h"
#import "JSVirtualMachine.h"
#import "JSExport.h"
对于这个五个类,简单总结如下
-
JSContext
是JavaScript的运行上下文,是执行JavaScript代码和注册native方法接口的执行环境。 -
JSValue
是JSContext
执行后的返回结果,可以是任何JavaScript类型,JSValue
提供很多的方法进行JavaScript和Objective-C类型的转换。但是需要注意的是JSValue
是一个强引用类型,JavaScript内部通过垃圾回收机制进行内存管理。JavaScript和Objective-C类型的转换可以参考JSValue
提供的方法。 -
JSManagedValue
是JSValue
的封装,用它可以解决JavaScript和native代码之间循环引用的问题。 -
JSVirtualMachine
管理JavaScript运行时和管理JavaScript暴露的native对象的内存。一般情况下是不需要直接和这个类交流的。但是如果你需要并行执行JavaScript代码,JSContext
就需要运行在不同的JSVirtualMachine
之上。每一个JSVirtualMachine
有着自己的 -
JSExport
是一个协议,通过实现它可以完成把一个native对象暴漏给JavaScript。
引用一张图来说明上述类型的一些关系
Objective-C 使用JavaScript
JavaScript没有类的概念,可以通过原型继承的方式实现继承。Objective-C可以调用JavaScript的函数和JavaScript属性。这些函数和属性需要先以NSString
的形式被加载到JSContext
环境上去,执行成功后,如果有返回值的话,返回一个JSValue
对象。一般情况下,JavaScript文件都会存在本地,打包进你的程序中去,当然也可以从网络中获取JavaScript文件,加载到context中去,这就是我们讲的热跟新。
函数和属性
现有如下JavaScript代码
//jscore.js
var jsGlobleVar = "JS Demo";
function min(a,b){
return a-b;
};
那么这个min方法就可以在Objective-C中以类似key-value的形式被检索到,这个value就是一个JSValue
,然后调用JSValue
实例的callWithArguments
传入Objective-C的类型的参数,在JavaScript环境中会进行相应的类型转换
//JSCoreViewController.m
NSString *jsCode = [self readFile]; // jscore.js
[self.context evaluateScript:jsCode]; //将上述min函数加载到context环境中去
JSValue *min = [self.context[@"min"] callWithArguments:@[@2,@4]]; // self.context[@"min"] 对应的就是js中的 min函数或者min属性
JSValue *jsVar = self.context[@"jsGlobleVar"];
NSLog(@"jsGlobleVar-----%@",[jsVar toString]);// "JS Demo"
NSLog(@"min+++++%d",[min toInt32]); //-2
JavaScript 调用 Objective-C
JavaScript调用Objective-C的block
如下代码,向context注册了key 为multi的 JSValue
对象
self.context[@"multi"] = ^(NSInteger a,NSInteger b){
return a*b;
};
相当在JavaScript中定义一个如下的函数
function multi (a,b){
return a*b;
}
那么在context调用这个block的方式和Objective-C中调用JavaScript的形式是一致的即
JSValue *multi = [self.context[@"multi"] callWithArguments:@[@3,@4]];
// 或者
multi = [self.context evaluateScript:@"multi(10,2)"];
自定义类型和方法
除了使用JSContext
下标方法暴露JavaScript对象以外,还可以使用JSExprot
协议把Objective-C中的自定义类转换为JSValue
,并暴露给JavaScript对象,在JavaScript的环境中操作这个Objective-C对象。
首先需要定义个遵循JSExport
的子协议。在子协议中规定了哪些属性和方法可以在JavaScript环境中可用。由于JavaScript中函数参数没有类型,所以所有的Objective-C方法都会以驼峰命名的方式被调用,比如-(void)minuse:(NSInteger)a b:(NSInteger)b c:(NSInteger)c;
将以minuseBC(a,b,c)
的方式被调用。也可以通过JSExportAs
宏重自定义在JavaScript中被调用的方法名//JSExportAs(add, - (NSInteger)add:(NSInteger)a b:(NSInteger)b)
。总结一下JSExport
的主要功能有
- 将Objective-C的函数和属性传给JavaScript
- @property---> JavaScript的getter/setter
- Objective-C 实例方法 ---> JavaScript 函数Objective-C
- Objective-C类方法----> 在全局类对象上的JavaScript函数
Objective-C对象
先定义如下的Objective-C协议,并写个类遵循这个协议(这里使用JSExportModel
来定义)
@protocol JSModelProtocol <JSExport>
-(void)minuse:(NSInteger)a b:(NSInteger)b c:(NSInteger)c;
@property (assign,nonatomic)NSInteger sum;
@end
接下来就可以将遵循了这个协议的类JSExportModel
的实例对象加到执行环境中去了,并调用在JavaScript中已经被加载到context中的useOCObject
方法。
-(void)excuteOCCdoeInJS{
JSExportModel *model = [JSExportModel new];
self.context[@"model"] = model;
[self.context[@"useOCObject"] callWithArguments:nil];
NSLog(@"%ld",(long)model.intV);//14
}
useOCObject的JavaScript代码如下
function useOCObject(){
model.minuseBC(100,12,12);
model.intV = 14;
};
这里调用了Objective-C的对象的方法,并对传入的对象的属性进行了修改。
传入一个类对象
因为JavaScript没有类的概念,所有传入到JavaScript环境中的Objective-C类就变为了一个Constructor
对象,如果你需要在JavaScript环境中生成一个对象,你需要使这个Objective-C类有个类方法来生成实例对象,即在之前的协议中,我们先定义一个类方法用来生成实例对象
//在协议中声明,来暴露给JavaScript
+(instancetype)createWithIntV:(NSInteger)value;
然后在Objective-C中进行如下调用
-(void)excuteOCClassInJS{
self.context[@"Model"] = [JSExportModel class];
JSValue *returned = [self.context[@"useOCClass"] callWithArguments:nil];
JSExportModel *m= [returned toObjectOfClass:[JSExportModel class]];
NSLog(@"%ld",(long)m.intV); //12
}
JavaScript代码如下所示
function useOCClass(){
var m = Model.createWithIntV(12);
m.minuseBC(10,1,1); // 调用Objective-C方法
return m
}
可以看到我们,上述Objective-C代码打印输出了12。
返回一个JavaScript函数
在JavaScript中函数也是变量,是一等公民,也可以被返回。在JavaScript文件中有个JavaScriptFunc函数,这个函数如下所示
function callback (){
// 这里打印的东西可以在 safari浏览器上的开发选项中打开
console.log("method----");
};
function jsFunc(){
Obj.jsValue = callback // 直接对变量赋值
return callback; //将function 以callback的形式返回
}
这个函数就是将callback函数返回给Objective-C对象。上述第一种是将
函数对象直接赋值给Objective-C对象,第二种直接返回函数对象。可以在Objective-C中用如下方式调用函数。
-(void)jsReturnBlock{
self.obj.jsValue = [self.context[@"jsFunc"] callWithArguments:nil];
[ self.obj.jsValue callWithArguments:nil];
self.context[@"Obj"] = self.obj;
[self.context[@"jsFunc"] callWithArguments:nil];
[ self.obj.jsValue callWithArguments:nil];
}
内存管理
我们都知道Objective-C使用ARC进行内存管理,JavaScriptCore(virtualMechine)内部通过垃圾回收机制来进行内存管理,所有的引用都是强引用。JavaScriptCore内部则保证了大多数的内存管理都是自动进行的,不需要我们进行额外的内存管理。但是还是得注意两种情况下的内存管理
- 在Objective-C对象中存储JavaScript的值
- 将JavaScript区域(主要是函数)加到Objective-C对象上中去
self.jsValue = [JSValue new];
self.context[@"block"] = ^(){
JSValue *value = self.jsValue;
NSLog(@"%@",value);
};
这里和普通block对象造成循环引用的类似,self.context 引用self,而本身self保有context。这种情况下,其实编译器也会告诉你这里产生了循环引用。 最好的方法就是将这个jsValue作为block的参数传到JavaScript环境中去。
还有一种情况就是在block中需要使用JS中其他的对象或者函数,需要从当前的JSContext
获取,这时候就造成了循环引用,推荐的方式则是通过+[JSContext currentContext]
来获取当前的context,即
self.jsValue = [JSValue new];
self.context[@"block"] = ^(){
// 循环引用
// context = self.context;
JSContext *context = [JSContext currentContext];
NSLog(@"%@",value);
};
还有一种情况是你必须要用一个Objective-C对象去保存一个JavaScript的值或者函数,你必须使用JSManagedValue
去弱引用JavaScript对象。JSManagedValue
本身就是一个弱引用JavaScript对象的对象。-addManagedReference:withOwner:
将JSManagedValue
对象加入到了垃圾回收的引用。这个函数的意思就是JavaScript的垃圾回收机制观察引用Objective-C对象ower,如果存在,那么它就不回收这个对象,否则就回收这个对象。
线程
JSVirtualMachine
保证了JavaScript代码执行的线程安全,锁都是JSVirtualMachine
来控制。使用不同的JSVirtualMachine
来实现串行或并发。
JavaScriptCore 和UIWebView
在UIWebView
中能够调用JavaScript代码,其本质上还是UIWebView
将JavaScript代码在其内部的JSContext
上执行。可以在UIWebView
加载完后调用,获取到这个_context。
-(void)webViewDidFinishLoad:(UIWebView *)webView{
_context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
[_context evaluateScript:@"showWebViewAlert()"];
}
网上有说documentView.webView.mainFrame.javaScriptContext
是私有API,会有被拒的风险,需谨慎。
JavaScriptCore 和WKWebView
WKWebView
和JavaScriptCore
之间的通信,可以通过WKWebView
的代理实现。在js代码中 通过发送如下 消息
window.webkit.messageHandlers.AppModel.postMessage({body: 'call js alert in js'});
WKWebView
只需要在启动的时候绑定了AppModel就可以接收到JS发过来的消息了。具体如下
WKUserContentController *contentVC = [WKUserContentController new];
//然后就可以通过代理取到js发来的消息了
[contentVC addScriptMessageHandler:self name:@"AppModel"];
config.userContentController = contentVC;
这样就可以WKScriptMessageHandler
受到消息了
// MARK: - WKScriptMessageHandler
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
// 收到消息
// window.webkit.messageHandlers.AppModel.postMessage({body: 'call js alert in js'});
NSLog(@"%@",message.body);
}
demo
demo下载地址iOS 中使用JS的demo ,喜欢的就star一下吧。。哈哈
参考资料
JavaScriptCore
JavaScriptCore by Example
JavaScriptCore and iOS 7
JavaScriptCore Tutorial for iOS: Getting Started
Integrating JavaScript into Native Apps