OC與JS交互

一、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;

使用 WKWebView 問題及解決方案

四、第三方庫(WebViewJavascriptBridge)

Github地址

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。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容