OC与JS交互

作者: 咖咖嘻 | 来源:发表于2018-04-20 18:13 被阅读110次

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

    相关文章

      网友评论

        本文标题:OC与JS交互

        本文链接:https://www.haomeiwen.com/subject/tbyzkftx.html