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阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容