JavaScriptCore
JavaScriptCore是webkit的一個(gè)重要組成部分,主要是對(duì)JS進(jìn)行解析和提供執(zhí)行環(huán)境。代碼是開(kāi)源的,可以下下來(lái)看看(源碼)。iOS7后蘋(píng)果在iPhone平臺(tái)推出,極大的方便了我們對(duì)js的操作。我們可以脫離webview直接運(yùn)行我們的js。iOS7以前我們對(duì)JS的操作只有webview里面一個(gè)函數(shù) stringByEvaluatingJavaScriptFromString,JS對(duì)OC的回調(diào)都是基于URL的攔截進(jìn)行的操作。大家用得比較多的是WebViewJavascriptBridge和EasyJSWebView這兩個(gè)開(kāi)源庫(kù),很多混合都采用的這種方式。
JavaScriptCore和我們相關(guān)的類不是很多,使用起來(lái)也非常簡(jiǎn)單。
#import "JSContext.h"
#import "JSValue.h"
#import "JSManagedValue.h"
#import "JSVirtualMachine.h"
#import "JSExport.h"
JSContext
JS執(zhí)行的環(huán)境,同時(shí)也通過(guò)JSVirtualMachine管理著所有對(duì)象的生命周期,每個(gè)JSValue都和JSContext相關(guān)聯(lián)并且強(qiáng)引用context。
JSValue
JS對(duì)象在JSVirtualMachine中的一個(gè)強(qiáng)引用,其實(shí)就是Hybird對(duì)象。我們對(duì)JS的操作都是通過(guò)它。并且每個(gè)JSValue都是強(qiáng)引用一個(gè)context。同時(shí),OC和JS對(duì)象之間的轉(zhuǎn)換也是通過(guò)它,相應(yīng)的類型轉(zhuǎn)換如下:
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)
JSManagedValue
JS和OC對(duì)象的內(nèi)存管理輔助對(duì)象。由于JS內(nèi)存管理是垃圾回收,并且JS中的對(duì)象都是強(qiáng)引用,而OC是引用計(jì)數(shù)。如果雙方相互引用,勢(shì)必會(huì)造成循環(huán)引用,而導(dǎo)致內(nèi)存泄露。我們可以用JSManagedValue保存JSValue來(lái)避免。
JSVirtualMachine
JS運(yùn)行的虛擬機(jī),有獨(dú)立的堆空間和垃圾回收機(jī)制。
JSExport
一個(gè)協(xié)議,如果JS對(duì)象想直接調(diào)用OC對(duì)象里面的方法和屬性,那么這個(gè)OC對(duì)象只要實(shí)現(xiàn)這個(gè)JSExport協(xié)議就可以了。
OC和JS之間的通信
兩者之間的通信還是很簡(jiǎn)單的,直接看簡(jiǎn)單代碼示例吧。
Objective-C -> JavaScript
self.context = [[JSContext alloc] init];
NSString *js = @"function add(a,b) {return a+b}";
[self.context evaluateScript:js];
JSValue *n = [self.context[@"add"] callWithArguments:@[@2, @3]];
NSLog(@"---%@", @([n toInt32]));//---5
步驟很簡(jiǎn)單,創(chuàng)建一個(gè)JSContext對(duì)象,然后將JS代碼加載到context里面,最后取到這個(gè)函數(shù)對(duì)象,調(diào)用callWithArguments這個(gè)方法進(jìn)行參數(shù)傳值。(JS里面函數(shù)也是對(duì)象)
JavaScript -> Objective-C
JS調(diào)用OC有兩個(gè)方法:block和JSExport protocol。
block(JS function):
self.context = [[JSContext alloc] init];
self.context[@"add"] = ^(NSInteger a, NSInteger b) {
NSLog(@"---%@", @(a + b));
};
[self.context evaluateScript:@"add(2,3)"];
我們定義一個(gè)block,然后保存到context里面,其實(shí)就是轉(zhuǎn)換成了JS的function。然后我們直接執(zhí)行這個(gè)function,調(diào)用的就是我們的block里面的內(nèi)容了。
JSExport protocol:
//定義一個(gè)JSExport protocol
@protocol JSExportTest <JSExport>
- (NSInteger)add:(NSInteger)a b:(NSInteger)b;
@property (nonatomic, assign) NSInteger sum;
@end
//建一個(gè)對(duì)象去實(shí)現(xiàn)這個(gè)協(xié)議:
@interface JSProtocolObj : NSObject<JSExportTest>
@end
@implementation JSProtocolObj
@synthesize sum = _sum;
//實(shí)現(xiàn)協(xié)議方法
- (NSInteger)add:(NSInteger)a b:(NSInteger)b
{
return a+b;
}
//重寫(xiě)setter方法方便打印信息,
- (void)setSum:(NSInteger)sum
{
NSLog(@"--%@", @(sum));
_sum = sum;
}
@end
//在VC中進(jìn)行測(cè)試
@interface ViewController () <JSExportTest>
@property (nonatomic, strong) JSProtocolObj *obj;
@property (nonatomic, strong) JSContext *context;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//創(chuàng)建context
self.context = [[JSContext alloc] init];
//設(shè)置異常處理
self.context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
[JSContext currentContext].exception = exception;
NSLog(@"exception:%@",exception);
};
//將obj添加到context中
self.context[@"OCObj"] = self.obj;
//JS里面調(diào)用Obj方法,并將結(jié)果賦值給Obj的sum屬性
[self.context evaluateScript:@"OCObj.sum = OCObj.addB(2,3)"];
}
demo很簡(jiǎn)單,還是定義了一個(gè)兩個(gè)數(shù)相加的方法,還有一個(gè)保存結(jié)果的變量。在JS中進(jìn)行調(diào)用這個(gè)對(duì)象的方法,并將結(jié)果賦值sum。唯一要注意的是OC的函數(shù)命名和JS函數(shù)命名規(guī)則問(wèn)題。協(xié)議中定義的是add: b:,但是JS里面方法名字是addB(a,b)??梢酝ㄟ^(guò)JSExportAs這個(gè)宏轉(zhuǎn)換成JS的函數(shù)名字。
修改下代碼:
@protocol JSExportTest <JSExport>
//用宏轉(zhuǎn)換下,將JS函數(shù)名字指定為add;
JSExportAs(add, - (NSInteger)add:(NSInteger)a b:(NSInteger)b);
@property (nonatomic, assign) NSInteger sum;
@end
//調(diào)用
[self.context evaluateScript:@"OCObj.sum = OCObj.add(2,3)"];
我們可以定義自己的異常捕獲,可以把context,異常block改為自己的:
self.context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
[JSContext currentContext].exception = exception;
NSLog(@"exception:%@",exception);
};
內(nèi)存管理
現(xiàn)在來(lái)說(shuō)說(shuō)內(nèi)存管理的注意點(diǎn),OC使用的ARC,JS使用的是垃圾回收機(jī)制,并且所有的引用是都強(qiáng)引用,不過(guò)JS的循環(huán)引用,垃圾回收會(huì)幫它們打破。JavaScriptCore里面提供的API,正常情況下,OC和JS對(duì)象之間內(nèi)存管理都無(wú)需我們?nèi)リP(guān)心。不過(guò)還是有幾個(gè)注意點(diǎn)需要我們?nèi)チ粢庀隆?/p>
1、不要在block里面直接使用context,或者使用外部的JSValue對(duì)象。
//錯(cuò)誤代碼:
self.context[@"block"] = ^(){
JSValue *value = [JSValue valueWithObject:@"aaa" inContext:self.context];
};
這個(gè)代碼,不用自己看了,編譯器都會(huì)提示你的。這個(gè)block里面使用self,很容易就看出來(lái)了。
//一個(gè)比較隱蔽的
JSValue *value = [JSValue valueWithObject:@"ssss" inContext:self.context];
self.context[@"log"] = ^(){
NSLog(@"%@",value);
};
這個(gè)是block里面使用了外部的value,value對(duì)context和它管理的JS對(duì)象都是強(qiáng)引用。這個(gè)value被block所捕獲,這邊同樣也會(huì)內(nèi)存泄露,context是銷毀不掉的。
//正確的做法,str對(duì)象是JS那邊傳遞過(guò)來(lái)。
self.context[@"log"] = ^(NSString *str){
NSLog(@"%@",str);
};
2、OC對(duì)象不要用屬性直接保存JSValue對(duì)象,因?yàn)檫@樣太容易循環(huán)引用了。
看個(gè)demo,把上面的示例改下:
//定義一個(gè)JSExport protocol
@protocol JSExportTest <JSExport>
//用來(lái)保存JS的對(duì)象
@property (nonatomic, strong) JSvalue *jsValue;
@end
//建一個(gè)對(duì)象去實(shí)現(xiàn)這個(gè)協(xié)議:
@interface JSProtocolObj : NSObject<JSExportTest>
@end
@implementation JSProtocolObj
@synthesize jsValue = _jsValue;
@end
//在VC中進(jìn)行測(cè)試
@interface ViewController () <JSExportTest>
@property (nonatomic, strong) JSProtocolObj *obj;
@property (nonatomic, strong) JSContext *context;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//創(chuàng)建context
self.context = [[JSContext alloc] init];
//設(shè)置異常處理
self.context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
[JSContext currentContext].exception = exception;
NSLog(@"exception:%@",exception);
};
//加載JS代碼到context中
[self.context evaluateScript:
@"function callback (){};
function setObj(obj) {
this.obj = obj;
obj.jsValue=callback;
}"];
//調(diào)用JS方法
[self.context[@"setObj"] callWithArguments:@[self.obj]];
}
上面的例子很簡(jiǎn)單,調(diào)用JS方法,進(jìn)行賦值,JS對(duì)象保留了傳進(jìn)來(lái)的obj,最后,JS將自己的回調(diào)callback賦值給了obj,方便obj下次回調(diào)給JS;由于JS那邊保存了obj,而且obj這邊也保留了JS的回調(diào)。這樣就形成了循環(huán)引用。
怎么解決這個(gè)問(wèn)題?我們只需要打破obj對(duì)JSValue對(duì)象的引用即可。當(dāng)然,不是我們OC中的weak。而是之前說(shuō)的內(nèi)存管理輔助對(duì)象JSManagedValue。
JSManagedValue 本身就是我們需要的弱引用。用官方的話來(lái)說(shuō)叫garbage collection weak reference。但是它幫助我們持有JSValue對(duì)象必須同時(shí)滿足一下兩個(gè)條件(不翻譯了,翻譯了怪怪的!):
The JSManagedValue's JavaScript value is reachable from JavaScript
The owner of the managed reference is reachable in Objective-C. Manually adding or removing the managed reference in the JSVirtualMachine determines reachability.
意思很簡(jiǎn)單,JSManagedValue 幫助我們保存JSValue,那里面保存的JS對(duì)象必須在JS中存在,同時(shí) JSManagedValue 的owner在OC中也存在。我們可以通過(guò)它提供的兩個(gè)方法``` + (JSManagedValue *)managedValueWithValue:(JSValue *)value;
- (JSManagedValue *)managedValueWithValue:(JSValue *)value andOwner:(id)owner
創(chuàng)建JSManagedValue對(duì)象。通過(guò)JSVirtualMachine的方法- (void)addManagedReference:(id)object withOwner:(id)owner來(lái)建立這個(gè)弱引用關(guān)系。通過(guò)- (void)removeManagedReference:(id)object withOwner:(id)owner``` 來(lái)手動(dòng)移除他們之間的聯(lián)系。
把剛剛的代碼改下:
//定義一個(gè)JSExport protocol
@protocol JSExportTest <JSExport>
//用來(lái)保存JS的對(duì)象
@property (nonatomic, strong) JSValue *jsValue;
@end
//建一個(gè)對(duì)象去實(shí)現(xiàn)這個(gè)協(xié)議:
@interface JSProtocolObj : NSObject<JSExportTest>
//添加一個(gè)JSManagedValue用來(lái)保存JSValue
@property (nonatomic, strong) JSManagedValue *managedValue;
@end
@implementation JSProtocolObj
@synthesize jsValue = _jsValue;
//重寫(xiě)setter方法
- (void)setJsValue:(JSValue *)jsValue
{
_managedValue = [JSManagedValue managedValueWithValue:jsValue];
[[[JSContext currentContext] virtualMachine] addManagedReference:_managedValue
withOwner:self];
}
@end
//在VC中進(jìn)行測(cè)試
@interface ViewController () <JSExportTest>
@property (nonatomic, strong) JSProtocolObj *obj;
@property (nonatomic, strong) JSContext *context;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//創(chuàng)建context
self.context = [[JSContext alloc] init];
//設(shè)置異常處理
self.context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
[JSContext currentContext].exception = exception;
NSLog(@"exception:%@",exception);
};
//加載JS代碼到context中
[self.context evaluateScript:
@"function callback (){};
function setObj(obj) {
this.obj = obj;
obj.jsValue=callback;
}"];
//調(diào)用JS方法
[self.context[@"setObj"] callWithArguments:@[self.obj]];
}
注:以上代碼只是為了突出用 JSManagedValue來(lái)保存 JSValue,所以重寫(xiě)了 setter 方法。實(shí)際不會(huì)寫(xiě)這么搓的姿勢(shì)。。。應(yīng)該根據(jù)回調(diào)方法傳進(jìn)來(lái)參數(shù),進(jìn)行保存 JSValue。
3、不要在不同的 JSVirtualMachine 之間進(jìn)行傳遞JS對(duì)象。
一個(gè) JSVirtualMachine可以運(yùn)行多個(gè)context,由于都是在同一個(gè)堆內(nèi)存和同一個(gè)垃圾回收下,所以相互之間傳值是沒(méi)問(wèn)題的。但是如果在不同的 JSVirtualMachine傳值,垃圾回收就不知道他們之間的關(guān)系了,可能會(huì)引起異常。
線程
JavaScriptCore 線程是安全的,每個(gè)context運(yùn)行的時(shí)候通過(guò)lock關(guān)聯(lián)的JSVirtualMachine。如果要進(jìn)行并發(fā)操作,可以創(chuàng)建多個(gè)JSVirtualMachine實(shí)例進(jìn)行操作。
與UIWebView的操作
通過(guò)上面的demo,應(yīng)該差不多了解OC如何和JS進(jìn)行通信。下面我們看看如何對(duì) UIWebView 進(jìn)行操作,我們不再通過(guò)URL攔截,我們直接取 UIWebView 的 context,然后進(jìn)行對(duì)JS操作。
在UIWebView的finish的回調(diào)中進(jìn)行獲取
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
}
上面用了私有屬性,可能會(huì)被蘋(píng)果給拒了。這邊要注意的是每個(gè)頁(yè)面加載完都是一個(gè)新的context,但是都是同一個(gè)JSVirtualMachine。如果JS調(diào)用OC方法進(jìn)行操作UI的時(shí)候,請(qǐng)注意線程是不是主線程。
參考: