一、iOS7 之前
1. OC 調用 JS
// 在 iOS7 之前,OC 調用 JS 只有一種方法,使用 UIWebView 的 stringByEvaluatingJavaScriptFromString:,因為涉及到 UI 更新,所以該方法只能在主線程中執行,另外, stringByEvaluatingJavaScriptFromString 是同步執行 JS 代碼,即會阻塞到該 JS 執行完畢,才繼續執行接下來的代碼。
dispatch_async(dispatch_get_main_queue(), ^{
NSString *jsString = [NSString stringWithFormat:@"alert(\"提示彈框\")"];
[webView stringByEvaluatingJavaScriptFromString:jsString];
});
2. JS 調用 OC
// 在 iOS7 之前,JS 調用 OC 主要是通過攔截 URL 請求,即 JS 發送一個偽 URL 請求,通過 webView 的代理方法進行監聽,根據 JS 與 OC 約定好的協議進行攔截,然后根據 URL 中的 path、query 等進行相應的處理。
// 主要通過 UIWebViewDelegate 中的 webView:shouldStartLoadWithRequest:navigationType: 方法攔截
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {
if([request.URL.scheme isEqualToString:@"js2oc"]) {
// oc 進行相應的處理操作
return NO;
}
return YES;
}
二、iOS7 之后 (JavaScriptCore)
iOS7 之后,蘋果官方引入了 JavaScriptCore 框架,使得 OC 可以在脫離 webView 的情況下直接運行 JS,而且,可以插入自定義 OC 對象到 JavaScript 環境中。
JavaScriptCore 框架中主要有以下幾個類:
JSContext: 主要提供在 OC 中執行 Java Script 代碼的環境,管理 Java Script Object 生命周期,每個 JSValue 都與 JSContext 強關聯,只要 JSValue 存在,JSContext 就保持引用,知道所有 JSValue 都被釋放,JSContext 才有可能被釋放。 一個 JSContext 是一個全局環境的實例。
JSValue: 是 JS value(JS 變量和方法) 的封裝,主要用于 JS 對象 與 OC 對象互相轉換。每個 JSValue 都和 JSContext 相關聯并且強引用 JSContext。
JSManagedValue: 是 JS 和 OC 對象的內存管理輔助對象,主要用來保存 JSValue,從而解決 OC 對象存儲 JSValue 導致循環引用問題。JS 內存管理是垃圾回收機制,其中所有對象都是強引用,但是我們不必擔心循環引用,因為垃圾回收會打破這種強引用;OC 是引用計數機制。JSValue 強引用相關 JSContext,把 OC 暴露給 JSContext,JSContext 強引用 OC,如果 OC 再強引用 JSValue 對象,就會導致循環引用,JSContext 釋放不了,內存泄漏。
為了解決 OC 與 JSValue 和 JSContext 的循環引用,引入了 JSManagedValue。
NSManagedValue *managedValue = [NSManagedValue managedValueWithValue:jsValue];
// managedValue 相當于弱引用 jsValue,如果 jsValue 指向 JSVirtualMachine 中 javascript value 被垃圾回收機制回收,jsValue 會自動設為 nil。
[jsVirtualMachine addManagedReference:managedValue withOwner:self];
// 該方法將原生的引用來告知 jsVirtualMachine,只要這種引用鏈存在,jsVirtualMachine 就不會對 managedValue.value 指向的 java script value 進行垃圾回收。
[jsVirtualMachine removeManagedReference:managedValue withOwner:self];
// 該方法在 jsVirtualMachine 中去除原生引用鏈,然后 java script value 就可能會被垃圾回收。
JSVirtualMachine: JS 運行的虛擬機,有獨立的堆空間和垃圾回收機制。主要用于多線程并發執行 JS 及 JS 與 OC 之間的內存管理。
每個 JSContext 屬于一個 JSVirtualMachine,每個 JSVirtualMachine 包含多個 JSContext,所以 屬于同一個 JSVirtualMachine 的 JSContext 可以互相傳值,因為共用相同的堆棧,而不同的 JSVirtualMachine 之間不能互相傳值。
如果想并發執行 JS,需要采用多個 JSVirtualMachine,每個 JSVirtualMachine 對應一個線程,同一個 JSVirtualMachine 中,只能串行執行 JS,當執行一個 JS 時,其他的需要等待。
JSExport: 是一個協議,這個協議將原生對象的屬性、方法暴露給 JavaScript,使得 JavaScript 可以直接調用 OC 對象的方法、屬性。遵守 JSExport 協議,就可以定義我們自己的協議,在協議中聲明的 API 都會暴露在 JS 中。
如果 JS 想調用 OC 對象的方法,只要使 OC 對象實現這個協議,并且將這個 OC 對象實例綁定到 JS。
1. 利用 JSContext 和 JSValue 實現 JS 與 OC 交互
HTML
<html>
<head>
<title>JS_OC</title>
</head>
<body>
<h1>發送偽URL請求</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 的運行運行環境 - JSContext。
- (void)webViewDidFinishLoad:(UIWebView *)webView {
self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
}
OC 調用 JS
JSValue *jsValue = [self.jsContext evaluateScript:@"oc_call_js_func"];
[jsValue callWithArguments:@[args,...]];
JS 調用 OC
// 即為 JS 調用 OC 的函數指定相應的 block
self.jsContext[@"js_call_oc_func"] = ^(args,...){
// 主線程執行 native UI 操作
}
2. 利用 JSExport 實現 JS 與 OC 交互
HTML
<html>
<head>
<title>JS_OC</title>
</head>
<body>
<h1>發送偽URL請求</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 文件可以看出來,JS 不是直接調用某一方法,而是調用某個對象 OCModel 的方法,只要創建一個 OC 對象 OCModel 并讓他實現 JS 要調用的方法,然后將它綁定到 JS 即可。
聲明一個 JSExport 協議,并在其中聲明 JS 調用 OC 的那些方法:
#import <JavaScriptCore/JavaScriptCore.h>
@protocol JSExportProtocol <JSExport>
- (void)callOCSystemCamera;
- (void)showOCAlertMsg:(NSString *)msg;
@end
指定類實現上面聲明的協議:
@interface OCModel : NSObject <JSExportProtocol>
@end
@implementaion OCModel
- (void)callOCSystemCamera {
// 主線程操作
}
- (void)showOCAlertMsg:(NSString *)msg {
// 主線程操作
}
@end
將上述類實例綁定到 JSContext 中:
- (void)webViewDidFinishLoad:(UIWebView *)webView {
JSContext *jsContext. = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
jsContext[@"OCModel"] = [OCModel new];
}
然后 JS 就可以通過 JSExport 協議調用 OC 的方法了。
注:JavaScriptCore 中,JS 是在子線程中調用 OC 方法,如果 OC 方法中有 UI 相關操作,需要在主線程中執行。
用 JavaScriptCore 進行 OC 與 JS 交互,又一個顯著的缺點:只有 html 加載完畢后,OC 才能調用 JS 成功
三、WKWebView
iOS8以后,蘋果推出了新框架 WebKit,提供了替換 UIWebView 的組件 WKWebView。WKWebView 在性能、穩定性和功能方面都有很大的提升,最顯著的優點就是占用的內存大幅減少。
WebKit 將 UIWebView 和 UIWebViewDelegate 重構為14個類和3個協議。具體參考
WKWebView: 用于顯示 web 內容。
WKWebViewConfiguration: 用于在初始化 WKWebView 時,指定其設置信息。
WKPreferences: 指定 WKWebView 的偏好設置。
WKScriptMessage: WKWebView 向 native 發送的消息。
WKUserScript: 注入 web view 的用戶腳本。
WKUserContentController: 主要用于向 web view 注入腳本和指定 web view 發送消息的接收處理(指定 JS 調用 OC 的實現代碼)。
UINavigation: 加載 web view 時返回的對象,主要用于跟蹤 web view 加載進程。
WKProcessPool、WKBackForwardList、WKBackForwardListItem等。
WKNavigationDelegate: 協議,主要用于處理 web view 的加載和跳轉。
WKUIDelegate: 協議,主要用于處理 JS 腳本,以及將 JS 的確認、警告等對話框用 native 表示。
WKScriptMessageHandler: 協議,主要用于接收、處理 web view 發送的消息。
1. 創建 WKWebView
// 初始化配置對象
WKWebviewConfiguration *config = [[WKWebViewConfiguration alloc] init];
// 初始化偏好設置
config.preferences = [[WKPreferences alloc] init];
// 指定最小字體,默認是 0
config.preferences.minimumFontSize = 10;
// 是否支持 javascript
config.preferences.javaScriptEnable = YES;
// javascript 不通過用戶交互是否可以自動打開窗口
config.preferences.javaScriptCanOpenWindowsAutomatically = YES;
// 創建 web view
WXWebView *webView = [[WKWebView alloc] initWithFrame:frame configuration:config];
webView.navigationDelegate = self;
webView.UIDelegate = self;
[webView loadRequest:urlRequest];
// 向 web view 中注入用戶腳本,可以通過該方法將 native 中的方法轉換為 JS 函數,比如,獲取 app 版本號等。
[webView.config.userContentController addUserScript:userScript];
// 指定 web view 發送消息的接收者(要及時執行 removeScriptMessageHandler:name 方法移除接收者,否者會循環引用而內存泄漏)
[webView.config.userContentController addScriptMessageHandler:self name:@"msgName"];
[self.view addSubview:webView];
2. JS 調用 OC
WKWebView 主要通過向 native 發送消息來調用 native 方法, native 根據接收到的消息進行相應的處理
// WKWebView 中 JS 發送消息
function clickAction() {
window.webkit.messageHandlers.msgName.postMessage(messageBody);
}
// native 主要通過 WKScriptMessageHandler 協議來接收消息,并進行處理
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if([message.name isEqualToString:@"msgName"]) {
// native action
}else {
// ...
}
}
3. OC 調用 JS
[webView evaluateJavaScript:jsString completionHandler^(id result, NSError *error){
// ...
}];
// 使用該方法執行 JS 腳本,或者直接執行 webView 暴露出來的全局函數,通常是后者。
4. WKUIDelegate 協議實現
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^)(void))completionHandler {
// 使用 UIAlertViewController 將 JS Alert 轉換為 native alert
}
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler {
// 將 JS 確認框轉換為 native 框。
}
//...其他的協議方法
5. WKNavigationDelegate 協議實現
// web view 開始接收 web content 時調用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation;
// 開始加載 web content 時調用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation;
// 當需要進行 server 重定向時調用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation;
// 當 web 需要進行驗證時調用
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler;
// web view 跳轉失敗時調用
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation
withError:(NSError *)error;
// web view 加載失敗時調用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation
withError:(NSError *)error;
// web view 跳轉結束時調用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation;
// web view 處理終止時調用
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView;
// web view 是否允許跳轉,比如點擊某個超鏈接時觸發,可以根據情況允許或者取消
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
// 已經知道響應結果,是否允許跳轉
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
四、第三方庫(WebViewJavascriptBridge)
WebViewJavascriptBridge 也是通過 URL 攔截來實現 JS 與 OC 的交互,而且同時支持 UIWebView、WKWebView。
優點:
html 加載時,只要 JS 代碼被運行就可以進行交互,不需等待 html 加載完畢才能交互。
iOS 與 Android 都有一套對應的庫,這樣 H5 只需要統一一套就行了。
缺點:
需要在 html 中加入固定的 JS 代碼片段。
1. JS 處理
主要包括兩個部分,固定聲明代碼、注冊 OC 需要調用的 JS 函數 和 JS 調用 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 內定義的對應 -->
setupWebViewJavascriptBridge(function(bridge) {
<!-- 注冊 OC 調用的 JS 函數 -->
bridge.registerHandler('OC2JS', function(data, responseCallback) {
//處理 OC 給的傳參
alert('OC 請求 JS 傳值參數是:'+data)
var responseData = { 'result':'handle success' }
// 將處理結果回傳給 OC
responseCallback(responseData)
})
var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
callbackButton.innerHTML = '點擊我,我會調用 OC 的方法'
callbackButton.onclick = function(e) {
e.preventDefault()
<!--JS 調用 OC -->
bridge.callHandler('loginAction', {'userId':'zhangsan','name': 'HeHe'}, function(response) {
// 處理 OC 回傳的數據
alert('收到 OC 的回調:'+response)
})
}
})
2. OC 處理
OC 中主要也是注冊 JS 調用的 OC 方法,和 聲明 OC 調用 JS 方法入口。
_bridge = [WebViewJavascriptBridge bridgeForWebView:_webView];
[_bridge setWebViewDelegate:self];
// 聲明 JS 調用的 OC 方法
[_bridge.registerHandler:@"JS2OC" handler:^(id data, WVJBResponseCallback responseCallback){、
// 對 JS 傳過來的 data 進行處理
// 將處理結果回傳給 JS
responseCallback(data);
}];
// 調用 JS
_bridge.callHandler:@"OC2JS" data:nil responseCallback:^(id responseData) {
// 處理 JS 回傳數據
}
3. WebViewJavascriptBridge 實現原理
分別在 OC 環境和 JS 環境都保存一個 bridge 對象,里面維持著 requestId、callbackId 以及每個Id對應的具體實現。
OC 通過 JS 環境的 window.WebViewJavascriptBridge 對象找到具體的方法,然后執行。
JS 通過改變 iframe 的 src 來喚起 webview 的代理方法 webView:(WKWebView* )webView decidePolicyForNavigationAction:(WKNavigationAction* )navigationAcion decisionHandler:(void(^)(WKNavigationActionPolicy))decisionHandler 或者 UIWebView 對應的代理方法,從而實現把 JS 消息發送給 OC。