一、iOS7 之前
1. OC 調(diào)用 JS
// 在 iOS7 之前,OC 調(diào)用 JS 只有一種方法,使用 UIWebView 的 stringByEvaluatingJavaScriptFromString:,因?yàn)樯婕暗?UI 更新,所以該方法只能在主線程中執(zhí)行肪笋,另外, stringByEvaluatingJavaScriptFromString 是同步執(zhí)行 JS 代碼度迂,即會(huì)阻塞到該 JS 執(zhí)行完畢藤乙,才繼續(xù)執(zhí)行接下來(lái)的代碼。
dispatch_async(dispatch_get_main_queue(), ^{
NSString *jsString = [NSString stringWithFormat:@"alert(\"提示彈框\")"];
[webView stringByEvaluatingJavaScriptFromString:jsString];
});
2. JS 調(diào)用 OC
// 在 iOS7 之前惭墓,JS 調(diào)用 OC 主要是通過(guò)攔截 URL 請(qǐng)求坛梁,即 JS 發(fā)送一個(gè)偽 URL 請(qǐng)求,通過(guò) webView 的代理方法進(jìn)行監(jiān)聽(tīng)腊凶,根據(jù) JS 與 OC 約定好的協(xié)議進(jìn)行攔截划咐,然后根據(jù) URL 中的 path、query 等進(jìn)行相應(yīng)的處理钧萍。
// 主要通過(guò) UIWebViewDelegate 中的 webView:shouldStartLoadWithRequest:navigationType: 方法攔截
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {
if([request.URL.scheme isEqualToString:@"js2oc"]) {
// oc 進(jìn)行相應(yīng)的處理操作
return NO;
}
return YES;
}
二褐缠、iOS7 之后 (JavaScriptCore)
iOS7 之后,蘋果官方引入了 JavaScriptCore 框架风瘦,使得 OC 可以在脫離 webView 的情況下直接運(yùn)行 JS队魏,而且,可以插入自定義 OC 對(duì)象到 JavaScript 環(huán)境中。
JavaScriptCore 框架中主要有以下幾個(gè)類:
JSContext: 主要提供在 OC 中執(zhí)行 Java Script 代碼的環(huán)境胡桨,管理 Java Script Object 生命周期官帘,每個(gè) JSValue 都與 JSContext 強(qiáng)關(guān)聯(lián),只要 JSValue 存在昧谊,JSContext 就保持引用刽虹,知道所有 JSValue 都被釋放,JSContext 才有可能被釋放呢诬。 一個(gè) JSContext 是一個(gè)全局環(huán)境的實(shí)例涌哲。
JSValue: 是 JS value(JS 變量和方法) 的封裝,主要用于 JS 對(duì)象 與 OC 對(duì)象互相轉(zhuǎn)換尚镰。每個(gè) JSValue 都和 JSContext 相關(guān)聯(lián)并且強(qiáng)引用 JSContext阀圾。
JSManagedValue: 是 JS 和 OC 對(duì)象的內(nèi)存管理輔助對(duì)象,主要用來(lái)保存 JSValue钓猬,從而解決 OC 對(duì)象存儲(chǔ) JSValue 導(dǎo)致循環(huán)引用問(wèn)題。JS 內(nèi)存管理是垃圾回收機(jī)制撩独,其中所有對(duì)象都是強(qiáng)引用敞曹,但是我們不必?fù)?dān)心循環(huán)引用,因?yàn)槔厥諘?huì)打破這種強(qiáng)引用综膀;OC 是引用計(jì)數(shù)機(jī)制澳迫。JSValue 強(qiáng)引用相關(guān) JSContext,把 OC 暴露給 JSContext剧劝,JSContext 強(qiáng)引用 OC橄登,如果 OC 再?gòu)?qiáng)引用 JSValue 對(duì)象,就會(huì)導(dǎo)致循環(huán)引用讥此,JSContext 釋放不了拢锹,內(nèi)存泄漏。
為了解決 OC 與 JSValue 和 JSContext 的循環(huán)引用萄喳,引入了 JSManagedValue卒稳。
NSManagedValue *managedValue = [NSManagedValue managedValueWithValue:jsValue];
// managedValue 相當(dāng)于弱引用 jsValue,如果 jsValue 指向 JSVirtualMachine 中 javascript value 被垃圾回收機(jī)制回收他巨,jsValue 會(huì)自動(dòng)設(shè)為 nil充坑。
[jsVirtualMachine addManagedReference:managedValue withOwner:self];
// 該方法將原生的引用來(lái)告知 jsVirtualMachine,只要這種引用鏈存在染突,jsVirtualMachine 就不會(huì)對(duì) managedValue.value 指向的 java script value 進(jìn)行垃圾回收捻爷。
[jsVirtualMachine removeManagedReference:managedValue withOwner:self];
// 該方法在 jsVirtualMachine 中去除原生引用鏈,然后 java script value 就可能會(huì)被垃圾回收份企。
JSVirtualMachine: JS 運(yùn)行的虛擬機(jī)也榄,有獨(dú)立的堆空間和垃圾回收機(jī)制。主要用于多線程并發(fā)執(zhí)行 JS 及 JS 與 OC 之間的內(nèi)存管理司志。
每個(gè) JSContext 屬于一個(gè) JSVirtualMachine手蝎,每個(gè) JSVirtualMachine 包含多個(gè) JSContext榕莺,所以 屬于同一個(gè) JSVirtualMachine 的 JSContext 可以互相傳值,因?yàn)楣灿孟嗤亩褩?媒椋煌?JSVirtualMachine 之間不能互相傳值钉鸯。
如果想并發(fā)執(zhí)行 JS,需要采用多個(gè) JSVirtualMachine邮辽,每個(gè) JSVirtualMachine 對(duì)應(yīng)一個(gè)線程唠雕,同一個(gè) JSVirtualMachine 中,只能串行執(zhí)行 JS吨述,當(dāng)執(zhí)行一個(gè) JS 時(shí)岩睁,其他的需要等待。
JSExport: 是一個(gè)協(xié)議揣云,這個(gè)協(xié)議將原生對(duì)象的屬性捕儒、方法暴露給 JavaScript,使得 JavaScript 可以直接調(diào)用 OC 對(duì)象的方法邓夕、屬性刘莹。遵守 JSExport 協(xié)議,就可以定義我們自己的協(xié)議焚刚,在協(xié)議中聲明的 API 都會(huì)暴露在 JS 中点弯。
如果 JS 想調(diào)用 OC 對(duì)象的方法,只要使 OC 對(duì)象實(shí)現(xiàn)這個(gè)協(xié)議矿咕,并且將這個(gè) OC 對(duì)象實(shí)例綁定到 JS抢肛。
1. 利用 JSContext 和 JSValue 實(shí)現(xiàn) JS 與 OC 交互
HTML
<html>
<head>
<title>JS_OC</title>
</head>
<body>
<h1>發(fā)送偽URL請(qǐng)求</h1>
<div style="margin-top: 10px">
<input type="button" value="Call OC With URL" onclick="callOC()">
</div>
<h3> JS Call OC Wth JavaScriptCore</h3>
<div style="margin-top: 20px">
<input type="button" value="Call OC System Camera" onclick="callOCSystemCamera()">
</div>
<div style="margin-top: 10px">
<input type="button" value="Call OC Alert" onclick="showOCAlertMsg('js title','js msg')">
</div>
</body>
<script>
function callOC(){
window.location.href = 'js2oc://callOC?p1=1&p2=2';
}
</script>
<script type="text/javascript">
function showJSAlertMsg(msg){
alert(msg);
}
</script>
</html>
UIWebView 加載完成后,獲取 JS 的運(yùn)行運(yùn)行環(huán)境 - JSContext碳柱。
- (void)webViewDidFinishLoad:(UIWebView *)webView {
self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
}
OC 調(diào)用 JS
JSValue *jsValue = [self.jsContext evaluateScript:@"oc_call_js_func"];
[jsValue callWithArguments:@[args,...]];
JS 調(diào)用 OC
// 即為 JS 調(diào)用 OC 的函數(shù)指定相應(yīng)的 block
self.jsContext[@"js_call_oc_func"] = ^(args,...){
// 主線程執(zhí)行 native UI 操作
}
2. 利用 JSExport 實(shí)現(xiàn) JS 與 OC 交互
HTML
<html>
<head>
<title>JS_OC</title>
</head>
<body>
<h1>發(fā)送偽URL請(qǐng)求</h1>
<div style="margin-top: 10px">
<input type="button" value="Call OC With URL" onclick="callOC()">
</div>
<h3> JS Call OC Wth JavaScriptCore</h3>
<div style="margin-top: 20px">
<input type="button" value="Call OC System Camera" onclick="OCModel.callOCSystemCamera()">
</div>
<div style="margin-top: 10px">
<input type="button" value="Call OC Alert" onclick="OCModel.showOCAlertMsg('js title','js msg')">
</div>
</body>
<script>
function callOC(){
window.location.href = 'js2oc://callOC?p1=1&p2=2';
}
</script>
<script type="text/javascript">
function showJSAlertMsg(msg){
alert(msg);
}
</script>
</html>
由 HTML 文件可以看出來(lái)捡絮,JS 不是直接調(diào)用某一方法,而是調(diào)用某個(gè)對(duì)象 OCModel 的方法莲镣,只要?jiǎng)?chuàng)建一個(gè) OC 對(duì)象 OCModel 并讓他實(shí)現(xiàn) JS 要調(diào)用的方法锦援,然后將它綁定到 JS 即可。
聲明一個(gè) JSExport 協(xié)議剥悟,并在其中聲明 JS 調(diào)用 OC 的那些方法:
#import <JavaScriptCore/JavaScriptCore.h>
@protocol JSExportProtocol <JSExport>
- (void)callOCSystemCamera;
- (void)showOCAlertMsg:(NSString *)msg;
@end
指定類實(shí)現(xiàn)上面聲明的協(xié)議:
@interface OCModel : NSObject <JSExportProtocol>
@end
@implementaion OCModel
- (void)callOCSystemCamera {
// 主線程操作
}
- (void)showOCAlertMsg:(NSString *)msg {
// 主線程操作
}
@end
將上述類實(shí)例綁定到 JSContext 中:
- (void)webViewDidFinishLoad:(UIWebView *)webView {
JSContext *jsContext. = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
jsContext[@"OCModel"] = [OCModel new];
}
然后 JS 就可以通過(guò) JSExport 協(xié)議調(diào)用 OC 的方法了灵寺。
注:JavaScriptCore 中,JS 是在子線程中調(diào)用 OC 方法区岗,如果 OC 方法中有 UI 相關(guān)操作略板,需要在主線程中執(zhí)行。
用 JavaScriptCore 進(jìn)行 OC 與 JS 交互慈缔,又一個(gè)顯著的缺點(diǎn):只有 html 加載完畢后叮称,OC 才能調(diào)用 JS 成功
三、WKWebView
iOS8以后,蘋果推出了新框架 WebKit瓤檐,提供了替換 UIWebView 的組件 WKWebView赂韵。WKWebView 在性能、穩(wěn)定性和功能方面都有很大的提升挠蛉,最顯著的優(yōu)點(diǎn)就是占用的內(nèi)存大幅減少祭示。
WebKit 將 UIWebView 和 UIWebViewDelegate 重構(gòu)為14個(gè)類和3個(gè)協(xié)議。具體參考
WKWebView: 用于顯示 web 內(nèi)容谴古。
WKWebViewConfiguration: 用于在初始化 WKWebView 時(shí)质涛,指定其設(shè)置信息。
WKPreferences: 指定 WKWebView 的偏好設(shè)置掰担。
WKScriptMessage: WKWebView 向 native 發(fā)送的消息汇陆。
WKUserScript: 注入 web view 的用戶腳本。
WKUserContentController: 主要用于向 web view 注入腳本和指定 web view 發(fā)送消息的接收處理(指定 JS 調(diào)用 OC 的實(shí)現(xiàn)代碼)带饱。
UINavigation: 加載 web view 時(shí)返回的對(duì)象毡代,主要用于跟蹤 web view 加載進(jìn)程。
WKProcessPool勺疼、WKBackForwardList教寂、WKBackForwardListItem等。
WKNavigationDelegate: 協(xié)議恢口,主要用于處理 web view 的加載和跳轉(zhuǎn)孝宗。
WKUIDelegate: 協(xié)議穷躁,主要用于處理 JS 腳本耕肩,以及將 JS 的確認(rèn)、警告等對(duì)話框用 native 表示问潭。
WKScriptMessageHandler: 協(xié)議猿诸,主要用于接收、處理 web view 發(fā)送的消息狡忙。
1. 創(chuàng)建 WKWebView
// 初始化配置對(duì)象
WKWebviewConfiguration *config = [[WKWebViewConfiguration alloc] init];
// 初始化偏好設(shè)置
config.preferences = [[WKPreferences alloc] init];
// 指定最小字體梳虽,默認(rèn)是 0
config.preferences.minimumFontSize = 10;
// 是否支持 javascript
config.preferences.javaScriptEnable = YES;
// javascript 不通過(guò)用戶交互是否可以自動(dòng)打開(kāi)窗口
config.preferences.javaScriptCanOpenWindowsAutomatically = YES;
// 創(chuàng)建 web view
WXWebView *webView = [[WKWebView alloc] initWithFrame:frame configuration:config];
webView.navigationDelegate = self;
webView.UIDelegate = self;
[webView loadRequest:urlRequest];
// 向 web view 中注入用戶腳本,可以通過(guò)該方法將 native 中的方法轉(zhuǎn)換為 JS 函數(shù)灾茁,比如窜觉,獲取 app 版本號(hào)等。
[webView.config.userContentController addUserScript:userScript];
// 指定 web view 發(fā)送消息的接收者(要及時(shí)執(zhí)行 removeScriptMessageHandler:name 方法移除接收者北专,否者會(huì)循環(huán)引用而內(nèi)存泄漏)
[webView.config.userContentController addScriptMessageHandler:self name:@"msgName"];
[self.view addSubview:webView];
2. JS 調(diào)用 OC
WKWebView 主要通過(guò)向 native 發(fā)送消息來(lái)調(diào)用 native 方法禀挫, native 根據(jù)接收到的消息進(jìn)行相應(yīng)的處理
// WKWebView 中 JS 發(fā)送消息
function clickAction() {
window.webkit.messageHandlers.msgName.postMessage(messageBody);
}
// native 主要通過(guò) WKScriptMessageHandler 協(xié)議來(lái)接收消息,并進(jìn)行處理
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if([message.name isEqualToString:@"msgName"]) {
// native action
}else {
// ...
}
}
3. OC 調(diào)用 JS
[webView evaluateJavaScript:jsString completionHandler^(id result, NSError *error){
// ...
}];
// 使用該方法執(zhí)行 JS 腳本拓颓,或者直接執(zhí)行 webView 暴露出來(lái)的全局函數(shù)语婴,通常是后者。
4. WKUIDelegate 協(xié)議實(shí)現(xiàn)
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^)(void))completionHandler {
// 使用 UIAlertViewController 將 JS Alert 轉(zhuǎn)換為 native alert
}
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler {
// 將 JS 確認(rèn)框轉(zhuǎn)換為 native 框。
}
//...其他的協(xié)議方法
5. WKNavigationDelegate 協(xié)議實(shí)現(xiàn)
// web view 開(kāi)始接收 web content 時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation;
// 開(kāi)始加載 web content 時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation;
// 當(dāng)需要進(jìn)行 server 重定向時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation;
// 當(dāng) web 需要進(jìn)行驗(yàn)證時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler;
// web view 跳轉(zhuǎn)失敗時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation
withError:(NSError *)error;
// web view 加載失敗時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation
withError:(NSError *)error;
// web view 跳轉(zhuǎn)結(jié)束時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation;
// web view 處理終止時(shí)調(diào)用
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView;
// web view 是否允許跳轉(zhuǎn)砰左,比如點(diǎn)擊某個(gè)超鏈接時(shí)觸發(fā)匿醒,可以根據(jù)情況允許或者取消
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
// 已經(jīng)知道響應(yīng)結(jié)果,是否允許跳轉(zhuǎn)
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
四缠导、第三方庫(kù)(WebViewJavascriptBridge)
WebViewJavascriptBridge 也是通過(guò) URL 攔截來(lái)實(shí)現(xiàn) JS 與 OC 的交互廉羔,而且同時(shí)支持 UIWebView、WKWebView酬核。
優(yōu)點(diǎn):
html 加載時(shí)蜜另,只要 JS 代碼被運(yùn)行就可以進(jìn)行交互,不需等待 html 加載完畢才能交互嫡意。
iOS 與 Android 都有一套對(duì)應(yīng)的庫(kù)举瑰,這樣 H5 只需要統(tǒng)一一套就行了。
缺點(diǎn):
需要在 html 中加入固定的 JS 代碼片段蔬螟。
1. JS 處理
主要包括兩個(gè)部分此迅,固定聲明代碼、注冊(cè) OC 需要調(diào)用的 JS 函數(shù) 和 JS 調(diào)用 OC 方法入口聲明旧巾。
<!-- 聲明交互 固定代碼 -->
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
<!-- 處理交互 方法名要和 iOS 內(nèi)定義的對(duì)應(yīng) -->
setupWebViewJavascriptBridge(function(bridge) {
<!-- 注冊(cè) OC 調(diào)用的 JS 函數(shù) -->
bridge.registerHandler('OC2JS', function(data, responseCallback) {
//處理 OC 給的傳參
alert('OC 請(qǐng)求 JS 傳值參數(shù)是:'+data)
var responseData = { 'result':'handle success' }
// 將處理結(jié)果回傳給 OC
responseCallback(responseData)
})
var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
callbackButton.innerHTML = '點(diǎn)擊我耸序,我會(huì)調(diào)用 OC 的方法'
callbackButton.onclick = function(e) {
e.preventDefault()
<!--JS 調(diào)用 OC -->
bridge.callHandler('loginAction', {'userId':'zhangsan','name': 'HeHe'}, function(response) {
// 處理 OC 回傳的數(shù)據(jù)
alert('收到 OC 的回調(diào):'+response)
})
}
})
2. OC 處理
OC 中主要也是注冊(cè) JS 調(diào)用的 OC 方法,和 聲明 OC 調(diào)用 JS 方法入口鲁猩。
_bridge = [WebViewJavascriptBridge bridgeForWebView:_webView];
[_bridge setWebViewDelegate:self];
// 聲明 JS 調(diào)用的 OC 方法
[_bridge.registerHandler:@"JS2OC" handler:^(id data, WVJBResponseCallback responseCallback){坎怪、
// 對(duì) JS 傳過(guò)來(lái)的 data 進(jìn)行處理
// 將處理結(jié)果回傳給 JS
responseCallback(data);
}];
// 調(diào)用 JS
_bridge.callHandler:@"OC2JS" data:nil responseCallback:^(id responseData) {
// 處理 JS 回傳數(shù)據(jù)
}
3. WebViewJavascriptBridge 實(shí)現(xiàn)原理
分別在 OC 環(huán)境和 JS 環(huán)境都保存一個(gè) bridge 對(duì)象,里面維持著 requestId廓握、callbackId 以及每個(gè)Id對(duì)應(yīng)的具體實(shí)現(xiàn)搅窿。
OC 通過(guò) JS 環(huán)境的 window.WebViewJavascriptBridge 對(duì)象找到具體的方法,然后執(zhí)行隙券。
JS 通過(guò)改變 iframe 的 src 來(lái)喚起 webview 的代理方法 webView:(WKWebView* )webView decidePolicyForNavigationAction:(WKNavigationAction* )navigationAcion decisionHandler:(void(^)(WKNavigationActionPolicy))decisionHandler 或者 UIWebView 對(duì)應(yīng)的代理方法男应,從而實(shí)現(xiàn)把 JS 消息發(fā)送給 OC。