iOS OC與JavaScript的交互
概念了解
JavaScriptCore
javaScriptCore是iOS7后推出的框架诫欠,是封裝了JavaScript和Objective-C橋接的Objective-C API,我們只需要只要用很少的代碼浴栽,就可以做到JavaScript調(diào)用Objective-C荒叼,或者Objective-C調(diào)用JavaScript。
JavaScriptCore中類及協(xié)議
- JSManagedValue:管理數(shù)據(jù)和方法的類
- JSContent:JS執(zhí)行的環(huán)境
- JSValue:JS和OC數(shù)據(jù)和方法的橋梁
- JSVirtualMachine:處理線程相關(guān)典鸡,使用較少
- JSExport:這是一個(gè)協(xié)議被廓,如果JS對(duì)象想直接調(diào)用OC對(duì)象里面的方法和屬性,那么這個(gè)OC對(duì)象只要實(shí)現(xiàn)這個(gè)JSExport協(xié)議就可以了萝玷。
代碼示例
我們先用終端創(chuàng)建個(gè)html文件拖入工程
test.html中代碼如下
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, target-densitydpi=device-dpi"/>
<title>JSCallOC</title>
<style>
*
{
//-webkit-tap-highlight-color: rgba(0,0,0,0);
text-decoration: none;
}
html,body
{
-webkit-touch-callout: none; /* prevent callout to copy image, etc when tap to hold */
-webkit-text-size-adjust: none; /* prevent webkit from resizing text to fit */
-webkit-user-select: none; /* prevent copy paste, to allow, change 'none' to 'text' */
}
#div-a
{
background:#FBA;
color:#FFF;
border-radius: 25px 5px;
}
</style>
<script type="text/javascript">
function showResult(resultNumber)
{
//alert(resultNumber);
document.getElementById("result").innerText = resultNumber;
}
function picCallBack(image) {
alert(image);
}
</script>
</head>
<body style="background:#CDE; color:#FFF">
<div>
<font size="3" color="black">輸入一個(gè)整數(shù):</font>
<textarea id="input" style="font-size:10pt;color:black;"></textarea>
</div>
<br/>
<div>
<font size="3" color="black">結(jié)果: <b id="result"> </b> </font>
</div>
<br/>
<div id="div-a">
<center>
<br/>
<input type="button" value="計(jì)算階乘" onclick="native.calculateForJS(input.value);" />
<br/>
<br/>
<input type="button" value="測(cè)試log" onclick="log('測(cè)試');" />
<br/>
<br/>
<input type="button" value="oc原生Alert" onclick="alert('alert');" />
<br/>
<br/>
<input type="button" value="addSubView" onclick="addSubView('view');" />
<br/>
<br/>
<input type="button" value="removeSubView" onclick="removeSubView('view');" />
<br/>
<br/>
<input type="button" value="多參數(shù)調(diào)用" onclick="mutiParams('參數(shù)1','參數(shù)2','參數(shù)3');" />
<br/>
<br/>
<input type="button" value="獲取照片" onclick="native.callCamera()" />
<br/>
<br/>
<a id="push" href="#" onclick="native.pushViewControllerTitle('SecondViewController','secondPushedFromJS');">
push to second ViewController
</a>
<br/>
<br/>
</center>
</div>
</body>
</html>
整個(gè)頁(yè)面均為HTML實(shí)現(xiàn)嫁乘,功能為:
1 計(jì)算階乘:輸入框輸入數(shù)字后調(diào)用OC中相關(guān)方法進(jìn)行計(jì)算,將計(jì)算結(jié)果顯示在HTML頁(yè)面上球碉。
2 測(cè)試log:點(diǎn)擊后蜓斧,在控制臺(tái)打印測(cè)試數(shù)據(jù)。
3 OC原生Alert:點(diǎn)擊后睁冬,彈出OC的提示框挎春。
4 addSubView:點(diǎn)擊后,在OC中添加一個(gè)View.
5 removeSubView: 點(diǎn)擊后豆拨,移除4中添加的View直奋。
6 多函數(shù)調(diào)用: 獲取HTML中的多個(gè)參數(shù)
7 獲取照片:訪問手機(jī)照片,并將選中照片顯示在HTML頁(yè)面上
8 push to Second View Controller:跳轉(zhuǎn)到下一個(gè)頁(yè)面施禾。
總結(jié):以上功能都是OC中獲取HTML按鈕中的相關(guān)點(diǎn)擊事件脚线,然后在OC中執(zhí)行相關(guān)代碼。
ViewController.m中代碼如下
#import "OneViewController.h"
#import <JavaScriptCore/JavaScriptCore.h>
#import "SecondViewController.h"
@protocol TestJSExport <JSExport>
/*
OC的函數(shù)命名和JS函數(shù)命名規(guī)則不同 我們可以通過JSExportAs這個(gè)宏優(yōu)化JS中調(diào)用的名稱
這個(gè)宏只對(duì)有參數(shù)的selector起作用
handleFactorialCalculateWithNumber:(NSNumber *)number作為 js方法:calculateForJS的別名*/
JSExportAs
(calculateForJS, - (void)handleFactorialCalculateWithNumber:(NSNumber *)number);
//- (void)calculateForJS:(NSNumber *)number;
//js方法
- (void)pushViewController:(NSString *)view title:(NSString *)title;
- (void)callCamera;
@end
@interface OneViewController ()<UIWebViewDelegate, TestJSExport>
@property (nonatomic, strong) UIWebView *webView;
@property (nonatomic, strong) JSContext *context;//給JavaScript提供運(yùn)行的上下文環(huán)境
@property (nonatomic, strong) UIView *addView;
@end
@implementation OneViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.webView];
NSString *path = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"test.html"];
NSString *htmlString = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
[_webView loadHTMLString:htmlString baseURL:nil];
}
- (UIWebView *)webView {
if (_webView == nil) {
_webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
_webView.delegate = self;
}
return _webView;
}
- (UIView *)addView {
if (_addView == nil) {
_addView =[[UIView alloc] initWithFrame:CGRectMake(10, 550, 200, 100)];
_addView.backgroundColor = [UIColor cyanColor];
}
return _addView;
}
#pragma mark UIWebViewDelegate
- (void)webViewDidFinishLoad:(UIWebView *)webView {
//將 html的title 設(shè)置為controller的title
self.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
//獲取當(dāng)前頁(yè)面的url
NSString *url = [webView stringByEvaluatingJavaScriptFromString:@"document.location.href"];
//這個(gè)好像是私有屬性 審核時(shí)可能被蘋果拒絕
self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//打印異常,由于JS的異常信息是不會(huì)在OC中被直接打印的,所以我們?cè)谶@里添加打印異常信息
self.context.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
context.exception = exceptionValue;
NSLog(@"exceptionValue --- %@",exceptionValue);
};
//以 JSExport 協(xié)議關(guān)聯(lián) native方法
self.context[@"native"] = self;
//以 block 形式關(guān)聯(lián) JavaScript function
self.context[@"log"] = ^(NSString *str) {
NSLog(@"%@",str);
};
//以 block 形式關(guān)聯(lián) JavaScript function
self.context[@"alert"] = ^(NSString *str) {
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertView *alter = [[UIAlertView alloc] initWithTitle:@"msg from js" message:str delegate:nil cancelButtonTitle:@"ok" otherButtonTitles:nil, nil];
[alter show];
});
};
//弱引用 避免循環(huán)引用
__block typeof(self) weakSelf = self;
self.context[@"addSubView"] = ^(NSString *viewName) {
[weakSelf.view addSubview:weakSelf.addView];
};
self.context[@"removeSubView"] = ^(NSString *viewName) {
[weakSelf.addView removeFromSuperview];
};
//多參數(shù)
self.context[@"mutiParams"] = ^(NSString *a, NSString *b, NSString *c) {
NSLog(@"%@ %@ %@",a,b,c);
};
}
#pragma mark - JSExport Methods
- (void)handleFactorialCalculateWithNumber:(NSNumber *)number{
NSLog(@"%@", number);
NSNumber *result = [self calculateFactorialOfNumber:number];
NSLog(@"%@", result);
[self.context[@"showResult"] callWithArguments:@[result]];
}
- (void)pushViewController:(NSString *)view title:(NSString *)title{
Class second = NSClassFromString(view);
id secondVC = [[second alloc]init];
((UIViewController*)secondVC).title = title;
[self.navigationController pushViewController:secondVC animated:YES];
}
// 假設(shè)此方法是在子線程中執(zhí)行的拾积,線程名sub-thread
- (void)callCamera {
// 這句假設(shè)要在主線程中執(zhí)行殉挽,線程名main-thread
NSLog(@"callCamera");
// 下面這兩句代碼最好還是要在子線程sub-thread中執(zhí)行啊
JSValue *picCallback = self.context[@"picCallBack"];
[picCallback callWithArguments:@[@"photos"]];
}
- (void)calculateForJS:(NSNumber *)number {
NSLog(@"點(diǎn)擊了計(jì)算階乘");
JSValue *showResult = self.context[@"showResult"];
[showResult callWithArguments:@[@"計(jì)算階乘"]];
}
#pragma mark - Factorial Method
- (NSNumber *)calculateFactorialOfNumber:(NSNumber *)number{
NSInteger i = [number integerValue];
if (i < 0){
return [NSNumber numberWithInteger:0];
}
if (i == 0){
return [NSNumber numberWithInteger:1];
}
NSInteger r = (i * [(NSNumber *)[self calculateFactorialOfNumber:[NSNumber numberWithInteger:(i - 1)]] integerValue]);
return [NSNumber numberWithInteger:r];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
self.context[@"native"] = nil;
}
@end
獲取HTML中的點(diǎn)擊事件
在HTML中丰涉,為一個(gè)元素添加點(diǎn)擊事件的兩種方法
第一種
<input type="button" value="計(jì)算階乘" onclick="native.calculateForJS(input.value);" />
在JS交互中,很多事情都是在webView的delegate方法中完成的斯碌,通過JSContent創(chuàng)建一個(gè)使用JS的環(huán)境一死,所以這里,我們先將self.content在這里面初始化;
#pragma mark UIWebViewDelegate
- (void)webViewDidFinishLoad:(UIWebView *)webView {
//這個(gè)好像是私有屬性 審核時(shí)可能被蘋果拒絕
self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//打印異常,由于JS的異常信息是不會(huì)在OC中被直接打印的,所以我們?cè)谶@里添加打印異常信息
self.context.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
context.exception = exceptionValue;
NSLog(@"exceptionValue --- %@",exceptionValue);
};
//以JSExport 協(xié)議關(guān)聯(lián) native 的方法
self.context[@"native"] = self;
}
我們需要聲明一個(gè)集成JSExport協(xié)議傻唾,協(xié)議中聲明JS使用的OC方法
@protocol TestJSExport <JSExport>
/*
OC的函數(shù)命名和JS函數(shù)命名規(guī)則不同 我們可以通過JSExportAs這個(gè)宏優(yōu)化JS中調(diào)用的名稱
這個(gè)宏只對(duì)有參數(shù)的selector起作用
handleFactorialCalculateWithNumber:(NSNumber *)number作為 js方法:calculateForJS的別名*/
JSExportAs
(calculateForJS, - (void)handleFactorialCalculateWithNumber:(NSNumber *)number);
@end
當(dāng)然你也可以按下面的寫法
@protocol TestJSExport <JSExport>
- (void)calculateForJS:(NSNumber *)number;
@end
第二種
<input type="button" value="oc原生Alert" onclick="alert('alert');" />
這種我們需要使用block的形式關(guān)聯(lián)JavaScript function
self.context[@"alert"] = ^(NSString *str) {
};
對(duì)HTML中的事件進(jìn)行處理
第一種 協(xié)議形式
我們協(xié)議中制定的方法名一定要和HTML中的方法名相同投慈。
當(dāng)我們協(xié)議需要使用JS中的方法時(shí),用下面的代碼進(jìn)行調(diào)用:
HTML中的方法
function showResult(resultNumber)
{
document.getElementById("result").innerText = resultNumber;
}
OC調(diào)用
JSValue *showResult = self.context[@"showResult"];
[showResult callWithArguments:@[@"計(jì)算階乘"]];
第二種 Block形式
注意避免循環(huán)引用冠骄,同時(shí)刷新UI的工作應(yīng)該放到主線程
self.context[@"alert"] = ^(NSString *str) {
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertView *alter = [[UIAlertView alloc] initWithTitle:@"msg from js" message:str delegate:nil cancelButtonTitle:@"ok" otherButtonTitles:nil, nil];
[alter show];
});
};
使用注意
OC調(diào)用JavaScript是同步伪煤,JavaScript調(diào)用OC是異步。
JavaScript調(diào)用本地方法是在子線程中執(zhí)行的凛辣,這里要根據(jù)實(shí)際情況考慮線程之間的切換抱既,而在回調(diào)JavaScript方法的時(shí)候最好是在剛開始調(diào)用此方法的線程中去執(zhí)行那段JavaScript方法的代碼,看下面的代碼解釋:
// 假設(shè)此方法是在子線程中執(zhí)行的扁誓,線程名sub-thread
- (void)callCamera {
// 這句假設(shè)要在主線程中執(zhí)行防泵,線程名main-thread
NSLog(@"callCamera");
// 下面這兩句代碼最好還是要在子線程sub-thread中執(zhí)行啊
JSValue *picCallback = self.context[@"picCallBack"];
[picCallback callWithArguments:@[@"photos"]];
}
本文demo: 點(diǎn)我下載
內(nèi)存管理陷阱
Objective-C的內(nèi)存管理機(jī)制是引用計(jì)數(shù),JavaScript的內(nèi)存管理機(jī)制是垃圾回收蝗敢。在大部分情況下捷泞,JavaScriptCore能做到在這兩種內(nèi)存管理機(jī)制之間無(wú)縫無(wú)錯(cuò)轉(zhuǎn)換,但也有少數(shù)情況需要特別注意寿谴。
在block內(nèi)捕獲JSContext
Block會(huì)為默認(rèn)為所有被它捕獲的對(duì)象創(chuàng)建一個(gè)強(qiáng)引用锁右。JSContext為它管理的所有JSValue也都擁有一個(gè)強(qiáng)引用。并且讶泰,JSValue會(huì)為它保存的值和它所在的Context都維持一個(gè)強(qiáng)引用咏瑟。這樣JSContext和JSValue看上去是循環(huán)引用的,然而并不會(huì)峻厚,垃圾回收機(jī)制會(huì)打破這個(gè)循環(huán)引用响蕴。
看下面的例子:
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谆焊,來防止循環(huán)引用惠桃。
JSManagedValue
當(dāng)把一個(gè)JavaScript值保存到一個(gè)本地實(shí)例變量上時(shí),需要尤其注意內(nèi)存管理陷阱辖试。 用實(shí)例變量保存一個(gè)JSValue非常容易引起循環(huán)引用辜王。
看以下下例子,自定義一個(gè)UIAlertView罐孝,當(dāng)點(diǎn)擊按鈕時(shí)調(diào)用一個(gè)JavaScript函數(shù):
#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的實(shí)現(xiàn)方法呐馆,MyAlertView需要持有successHandler,failureHandler這兩個(gè)JSValue對(duì)象
向JavaScript環(huán)境注入一個(gè)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];
};
因?yàn)镴avaScript環(huán)境中都是“強(qiáng)引用”(相對(duì)Objective-C的概念來說)的莲兢,這時(shí)JSContext強(qiáng)引用了一個(gè)presentNativeAlert函數(shù),這個(gè)函數(shù)中又強(qiáng)引用了MyAlertView 等于說JSContext強(qiáng)引用了MyAlertView搏讶,而MyAlertView為了持有兩個(gè)回調(diào)強(qiáng)引用了successHandler和failureHandler這兩個(gè)JSValue坯临,這樣MyAlertView和JavaScript環(huán)境互相引用了。
所以蘋果提供了一個(gè)JSMagagedValue類來解決這個(gè)問題坟岔。
看MyAlertView.m的正確實(shí)現(xiàn):
#import "MyAlertView.h"
@interface XorkAlertView() <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對(duì)象在類內(nèi)部使用JSManagedValue來保存摔桦。
JSManagedValue本身是一個(gè)弱引用對(duì)象社付,需要調(diào)用JSVirtualMachine的addManagedReference:withOwner:
把它添加到JSVirtualMachine對(duì)象中,確保使用過程中JSValue不會(huì)被釋放
當(dāng)用戶點(diǎn)擊AlertView上的按鈕時(shí)邻耕,根據(jù)用戶點(diǎn)擊哪一個(gè)按鈕鸥咖,來執(zhí)行對(duì)應(yīng)的處理函數(shù),這時(shí)AlertView也隨即被銷毀兄世。 這時(shí)需要手動(dòng)調(diào)用removeManagedReference:withOwner:
來移除JSManagedValue啼辣。
參考文章
http://www.reibang.com/p/cdaf9bc3d65d
https://hjgitbook.gitbooks.io/ios/content/04-technical-research/04-javascriptcore-note.html
http://my.oschina.net/whforever/blog/669813
http://www.reibang.com/p/f896d73c670a