前面講完攔截URL的方式實現JS與OC互相呼叫,終於到JavaScriptCore了。它是從iOS7開始加入的,用 Objective-C 把 WebKit 的 JavaScript 引擎封裝了一下,提供了簡單快捷的方式與JavaScript互動。 關於JavaScriptCore的使用有兩篇很好的文章:
看了上述兩篇文章,對JavaScriptCore應該已經基本瞭解了。我就簡要介紹一下,然後用程式碼來實際操作了。先上最終實現的效果:
![iOS下JS與OC互相呼叫(四)--JavaScriptCore](https://i.iter01.com/images/9c07ca33c9898095a847621a87542fb872dc92322d9e944a9a0096bb24cfcf56.gif)
1、簡要介紹JavaScriptCore
JavaScriptCore
是一個iOS 7 新新增的框架,使用前需要先匯入JavaScriptCore.framework
。
然後我們在JavaScriptCore.h
中可以看到,該框架主要的類就只有五個:
![JavaScriptCore.h](https://i.iter01.com/images/c950c0f2d80af788c16024e6e32cf02306d1cc02b93b6c2aa08d415a40833e6f.png)
1.1 JSVirtualMachine
JSVirtualMachine
看名字直譯是JS 虛擬機器,也就是說JavaScript是在一個虛擬的環境中執行,而JSVirtualMachine
為其執行提供底層資源。
![iOS下JS與OC互相呼叫(四)--JavaScriptCore](https://i.iter01.com/images/51bf0c4ed7cad0b2efe67479ce6f2a97751191bc09ef66167ff05db7009926a3.png)
翻譯這段描述:一個JSVirtualMachine
例項,代表一個獨立的JavaScript
物件空間,併為其執行提供資源。它通過加鎖虛擬機器,保證JSVirtualMachine
是執行緒安全的,如果要併發執行JavaScript
,那我們必須建立多個獨立的JSVirtualMachine
例項,在不同的例項中執行JavaScript
。
通過alloc/init
就可以建立一個新的JSVirtualMachine
物件。但是我們一般不用新建JSVirtualMachine
物件,因為建立JSContext時,如果我們不提供一個特性的JSVirtualMachine
,內部會自動建立一個JSVirtualMachine
物件。
1.2 JSContext
JSContext
是為JavaScript的執行提供執行環境,所有的JavaScript的執行都必須在JSContext
環境中。JSContext
也管理JSVirtualMachine
中物件的生命週期。每一個JSValue
物件都要強引用關聯一個JSContext
。當與某JSContext
物件關聯的所有JSValue
釋放後,JSContext
也會被釋放。
建立一個JSContext物件的方式有:
// 1.這種方式需要傳入一個JSVirtualMachine物件,如果傳nil,會導致應用崩潰的。
JSVirtualMachine *JSVM = [[JSVirtualMachine alloc] init];
JSContext *JSCtx = [[JSContext alloc] initWithVirtualMachine:JSVM];
// 2.這種方式,內部會自動建立一個JSVirtualMachine物件,可以通過JSCtx.virtualMachine
// 看其是否建立了一個JSVirtualMachine物件。
JSContext *JSCtx = [[JSContext alloc] init];
// 3. 通過webView的獲取JSContext。
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
複製程式碼
上面推薦的兩篇文章以及網上介紹JavaScriptCore的文章多是通過1和2這兩種方式建立JSContext,然後執行JavaScript,演示JavaScriptCore。我一直有疑問,如果不是HTML結合OC,才會使用到JavaScript,那在一個虛擬的環境裡執行JS有什麼意義。 所以,後面我是用方式3來建立JSContext。
1.3 JSValue
JSValue
都是通過JSContext
返回或者建立的,並沒有構造方法。JSValue
包含了每一個JavaScript型別的值,通過JSValue
可以將Objective-C中的型別轉換為JavaScript中的型別,也可以將JavaScript中的型別轉換為Objective-C中的型別。
上述兩篇文章中均有OC、JSValue、JavaScript的型別對應關係表。
![iOS下JS與OC互相呼叫(四)--JavaScriptCore](https://i.iter01.com/images/1848e16df602c81a1ee72bec7cae3dc63e017de8d7dc846037fe9e4bf8b11d27.png)
1.4 JSManagedValue
JSManagedValue
主要用途是解決JSValue物件在Objective-C 堆上的安全引用問題。把JSValue 儲存進Objective-C 堆物件中是不正確的,這很容易引發迴圈引用,而導致JSContext不能釋放。
這個類主要是將JSValue物件轉換為JSManagedValue的API,而且也不常用,就不做具體介紹了。以後遇到使用場景再補充。
1.5 JSExport
JSExport
是一個協議類,但是該協議並沒有任何屬性和方法。
怎麼使用呢?
我們可以自定義一個協議類,繼承自JSExport
。無論我們在JSExport
裡宣告的屬性,例項方法還是類方法,繼承的協議都會自動的提供給任何 JavaScript 程式碼。
So,我們只需要在自定義的協議類中,新增上屬性和方法就可以了。
2、程式碼操作展示
因為該系列主要是JS與OC互調,所以主要介紹如何用JavaScriptCore實現JS與OC互調。
2.1 建立UIWebView,並載入本地HTML。
這步跟 文章(一)中的步驟一是一樣的。
self.webView = [[UIWebView alloc] initWithFrame:self.view.frame];
self.webView.delegate = self;
NSURL *htmlURL = [[NSBundle mainBundle] URLForResource:@"index.html" withExtension:nil];
// NSURL *htmlURL = [NSURL URLWithString:@"http://www.baidu.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:htmlURL];
// 如果不想要webView 的回彈效果
self.webView.scrollView.bounces = NO;
// UIWebView 滾動的比較慢,這裡設定為正常速度
self.webView.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
[self.webView loadRequest:request];
[self.view addSubview:self.webView];
複製程式碼
HTML的內容也大致一樣,不過JS的呼叫有些區別,更簡單了。
function shareClick() {
share('測試分享的標題','測試分享的內容','url=http://www.baidu.com');
}
function shareResult(channel_id,share_channel,share_url) {
var content = channel_id+","+share_channel+","+share_url;
asyncAlert(content);
document.getElementById("returnValue").value = content;
}
function locationClick() {
getLocation();
}
function setLocation(location) {
asyncAlert(location);
document.getElementById("returnValue").value = location;
}
複製程式碼
更詳細的可以看demo中的HTML原始碼,demo地址在文章末。
2.2 新增JS要呼叫的原生OC方法。
在HMTL載入成功的回撥方法- (void)webViewDidFinishLoad:(UIWebView *)webView
中新增要呼叫的原生OC方法。
#pragma mark - UIWebViewDelegate
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
NSLog(@"webViewDidFinishLoad");
[self addCustomActions];
}
複製程式碼
將所有要新增的功能方法,集中到一個方法addCustomActions
中,便於維護。
#pragma mark - private method
- (void)addCustomActions
{
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
[self addScanWithContext:context];
[self addLocationWithContext:context];
[self addSetBGColorWithContext:context];
[self addShareWithContext:context];
[self addPayActionWithContext:context];
[self addShakeActionWithContext:context];
[self addGoBackWithContext:context];
}
複製程式碼
然後每一個小功能獨立開來,這樣修改和解決Bug的時候能夠快速定位到某個功能。
- (void)addShareWithContext:(JSContext *)context
{
__weak typeof(self) weakSelf = self;
context[@"share"] = ^() {
NSArray *args = [JSContext currentArguments];
if (args.count < 3) {
return ;
}
NSString *title = [args[0] toString];
NSString *content = [args[1] toString];
NSString *url = [args[2] toString];
// 在這裡執行分享的操作...
// 將分享結果返回給js
NSString *jsStr = [NSString stringWithFormat:@"shareResult('%@','%@','%@')",title,content,url];
[[JSContext currentContext] evaluateScript:jsStr];
};
}
複製程式碼
注意:
- 1.JS要呼叫的原生OC方法,可以在viewDidLoad webView被建立後就新增好,但最好是在網址載入成功後再新增,以避免無法預料的亂入Bug。
- 2.block 中的執行環境是在子執行緒中。奇怪的是竟然可以更新部分UI,例如給view設定背景色,呼叫webView執行js等,但是彈出原生alertView就會在控制檯報子執行緒操作UI的錯誤資訊。
- 3.避免迴圈引用,因為block 會持有外部變數,而JSContext也會強引用它所有的變數,因此在block中呼叫self時,要用__weak 轉一下。而且在block內不要使用外部的context 以及JSValue,都會導致迴圈引用。如果要使用context 可以使用
[JSContext currentContext]
。當然我們可以將JSContext 和JSValue當做block的引數傳進去,這樣就可以使用啦。
2.3 OC呼叫JS方法
OC呼叫JS方法就有多種方式了。首先介紹使用JavaScriptCore框架的方式。
** 方式1 **
使用JSContext的方法-evaluateScript
,可以實現OC呼叫JS方法。
下面是一個呼叫JS中payResult
方法的示例程式碼:
NSString *jsStr = [NSString stringWithFormat:@"payResult('%@')",@"支付成功"];
[[JSContext currentContext] evaluateScript:jsStr];
複製程式碼
** 方式2 **
使用JSValue的方法-callWithArguments
,也可以實現OC呼叫JS方法。
下面這個示例程式碼依然是呼叫JS中的payResult
:
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
[context[@"payResult"] callWithArguments:@[@"支付彈窗"]];
複製程式碼
當然,如果是在執行原生OC方法之後,想要在OC執行完操作後,將結果回撥給JS時,可以這樣寫:
- (void)addPayActionWithContext:(JSContext *)context
{
context[@"payAction"] = ^() {
NSArray *args = [JSContext currentArguments];
if (args.count < 4) {
return ;
}
NSString *orderNo = [args[0] toString];
NSString *channel = [args[1] toString];
long long amount = [[args[2] toNumber] longLongValue];
NSString *subject = [args[3] toString];
// 支付操作
NSLog(@"orderNo:%@---channel:%@---amount:%lld---subject:%@",orderNo,channel,amount,subject);
// 將支付結果返回給js
[[JSContext currentContext][@"payResult"] callWithArguments:@[@"支付成功"]];
};
}
複製程式碼
方式3
以前介紹過的,利用UIWebView的API。
NSString *jsStr = [NSString stringWithFormat:@"payResult('%@')",@"支付成功"];
[weakSelf.webView stringByEvaluatingJavaScriptFromString:jsStr];
複製程式碼
3、補充介紹JavaScriptCore
好處:使用JavaScriptCore,JS呼叫Native方法時,引數的傳遞更方便,不用擔心特殊符號的轉換問題。 不好的地方:只能使用在iOS 7以上。這點我相信現在基本沒有多少應用還相容iOS 6了吧,我去年在做這個功能的時候,還要相容iOS 6 ? ? 。
先把JS與OC互調部分的介紹完了,這裡再補充一些關於JavaScriptCore的相關知識。 在OC中如何往JS環境中新增一個變數,便於後續在JS中使用呢?
JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"var arr = [3, 4, 'abc'];"];
複製程式碼
而用到實際的UIWebView上,可以這樣:
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
[context evaluateScript:@"var arr = [3, 4, 'abc'];"];
複製程式碼
當上面這兩行程式碼執行完後,我點選HTML中的按鈕
<input type="button" value="輸出arr" onclick="showArr()" />
function showArr(){
asyncAlert(arr);
}
function asyncAlert(content) {
setTimeout(function(){
alert(content);
},1);
}
複製程式碼
直接輸出arr,結果是這樣的:
![iOS下JS與OC互相呼叫(四)--JavaScriptCore](https://i.iter01.com/images/2fdae35cbe4d987a4dc331b1c97db113b834b7b22c4073c27c6058815f35ca1c.png)
如果我們在OC中想要取出arr,只需要這樣:
JSValue *value = context[@"arr"];
複製程式碼
OC中的block可以傳入到JavaScript中,這樣就建立了一個新的JS方法。我們上面的JS呼叫OC方法,就是利用的這個實現的。
關於JSExport如何使用? JSExport 主要是用於將OC中定義的Model類等引入到JavaScript中,便於在JS中使用這種物件和物件的屬性、方法。
JSExport的大致使用流程是:
1.建立一個自定義協議
XXXExport
繼承自JSExport
。
2.在自定義的
XXXExport
中新增JS裡需要呼叫的屬性和方法。
3.在自定義的Model類中實現XXXExport中的屬性的get/set方法以及定義的方法。
4.通過JSContext將Model類或者Model類的例項插入到JavaScript中。
當然,我們也可以給已經存在的類動態新增協議,來使其可以供JS 使用。這些示例和示例程式碼,在文章NSHipster中文版的JavaScriptCore 和 JavaScriptCore框架在iOS7中的物件互動和管理中有很詳細的介紹和使用展示。
WKWebView 與JavaScriptCore
關於WKWebView 與JavaScriptCore,由於WKWebView 不支援通過如下的KVC的方式建立JSContext:
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
複製程式碼
那麼就不能在WKWebView中使用JavaScriptCore了。 而且,WKWebView中有OC 和JS互動的方式,更easy 、更簡潔,因此也用不著使用JavaScriptCore。 WKWebView中如何實現OC與JS互動可以看前面這篇文章:iOS下JS與OC互相呼叫(三)--MessageHandler
UIWebView利用JavaScriptCore來實現互動的示例工程:JS_OC_JavaScriptCore
Have Fun!