一、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。
网友评论