hybrid.jpgjavascript
前言html
Web 页面中的 JS 与 iOS Native 如何交互是每一个 iOS 猿必须掌握的技能。而说到 Native 与 JS 交互,就不得不提一嘴 Hybrid。前端
Hybrid 的翻译结果并非很文明(擦汗,不知道为啥不少翻译软件会译为“杂种”,但我更喜欢将它翻译为“混合、混血”),Hybrid Mobile App 我对它的理解为经过 Web 网络技术(如 HTML,CSS 和 JavaScript)与 Native 相结合的混合移动应用程序。html5
那么咱们来看一下 Hybrid 对比 Native 有哪些优劣:java
hybrid_vs_native.jpgios
由于 Hybrid 的灵活性(更改 Web 页面没必要从新发版)以及通用性(一份 H5 玩遍全部平台)再加上门槛低(前端猿能够无痛上手开撸)的优点,因此在非核心功能模块使用 Web 经过 Hybrid 的方式来实现可能从各方面都会优于 Native。而 Native 则能够在核心功能和设备硬件的调用上为 JS 提供强有力的支持。web
索引数组
Hybrid 的发展简史安全
JavaScriptCore 简介网络
iOS Native 与 JS 交互的方法
WKWebView 与 JS 交互的特有方法
JS 经过 Native 调用 iOS 设备摄像头的 Demo
总结
Hybrid 的发展简史
下面简述一下 Hybrid 的发展史:
1.H5 发布
html5.png
Html5 是在 2014 年 9 月份正式发布的,这一次的发布作了一个最大的改变就是“从之前的 XML 子集升级成为一个独立集合”。
2.H5 渗入 Mobile App 开发
Native APP 开发中有一个 webview 的组件(Android 中是 webview,iOS 有 UIWebview和 WKWebview),这个组件能够加载 Html 文件。
在 H5 大行其道以前,webview 加载的 web 页面很单调(由于只能加载一些静态资源),自从 H5 火了以后,前端猿们开发的 H5 页面在 webview 中的表现不俗使得 H5 开发慢慢渗透到了 Mobile App 开发中来。
3.Hybrid 现状
虽然目前已经出现了 RN 和 Weex 这些使用 JS 写 Native App 的技术,可是 Hybrid 仍然没有被淘汰,市面上大多数应用都不一样程度的引入了 Web 页面。
JavaScriptCore
JavaScriptCore 这个库是 Apple 在 iOS 7 以后加入到标准库的,它对 iOS Native 与 JS 作交互调用产生了划时代的影响。
JavaScriptCore 大致是由 4 个类以及 1 个协议组成的:
javascriptcore_framework.jpg
JSContext 是 JS 执行上下文,你能够把它理解为 JS 运行的环境。
JSValue 是对 JavaScript 值的引用,任何 JS 中的值均可以被包装为一个 JSValue。
JSManagedValue 是对 JSValue 的包装,加入了“conditional retain”。
JSVirtualMachine 表示 JavaScript 执行的独立环境。
还有 JSExport 协议:
实现将 Objective-C 类及其实例方法,类方法和属性导出为 JavaScript 代码的协议。
这里的 JSContext,JSValue,JSManagedValue 相对比较好理解,下面咱们把 JSVirtualMachine 单拎出来讲明一下:
JSVirtualMachine 的用法和其与 JSContext 的关系
jsvirtualmachine.jpg
官方文档的介绍:
JSVirtualMachine 实例表示用于 JavaScript 执行的独立环境。 您使用此类有两个主要目的:支持并发 JavaScript 执行,并管理 JavaScript 和 Objective-C 或 Swift 之间桥接的对象的内存。
关于 JSVirtualMachine 的使用,通常状况下咱们不用手动去建立 JSVirtualMachine。由于当咱们获取 JSContext 时,获取到的 JSContext 从属于一个 JSVirtualMachine。
每一个 JavaScript 上下文(JSContext 对象)都属于一个 JSVirtualMachine。 每一个 JSVirtualMachine 能够包含多个上下文,容许在上下文之间传递值(JSValue 对象)。 可是,每一个 JSVirtualMachine 是不一样的,即咱们不能将一个 JSVirtualMachine 中建立的值传递到另外一个 JSVirtualMachine 中的上下文。
JavaScriptCore API 是线程安全的 —— 例如,咱们能够从任何线程建立 JSValue 对象或运行 JS 脚本 - 可是,尝试使用相同 JSVirtualMachine 的全部其余线程将被阻塞。 要在多个线程上同时(并发)运行 JavaScript 脚本,请为每一个线程使用单独的 JSVirtualMachine 实例。
JSValue 与 JavaScript 的转换表
iOS Native 与 JS 交互
对于 iOS Native 与 JS 交互咱们先从调用方向上分为两种状况来看:
JS 调用 Native
Native 调用 JS
call-eachother.jpg
JS 调用 Native
其实 JS 调用 iOS Native 也分为两种实现方式:
假 Request 方法
JavaScriptCore 方法
假 Request 方法
原理:其实这种方式就是利用了 webview 的代理方法,在 webview 开始请求的时候截获请求,判断请求是否为约定好的假请求。若是是假请求则表示是 JS 想要按照约定调用咱们的 Native 方法,按照约定去执行咱们的 Native 代码就好。
UIWebView
UIWebView 代理有用于截获请求的函数,在里面作判断就好:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSURL *url = request.URL;
// 与约定好的函数名做比较
if ([[url scheme] isEqualToString:@"your_func_name"]) {
// just do it
}
}
WKWebView
WKWebView 有两个代理,一个是 WKNavigationDelegate,另外一个是 WKUIDelegate。WKUIDelegate 咱们在下面的章节会讲到,这里咱们须要设置并实现它的 WKNavigationDelegate 方法:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSURL *url = navigationAction.request.URL;
// 与约定好的函数名做比较
if ([[url scheme] isEqualToString:@"your_func_name"]) {
// just do it
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
decisionHandler(WKNavigationActionPolicyAllow);
}
Note: decisionHandler 是当你的应用程序决定是容许仍是取消导航时,要调用的代码块。 该代码块使用单个参数,它必须是枚举类型 WKNavigationActionPolicy 的常量之一。若是不调用 decisionHandler 会引发 crash。
这里补充一下 JS 代码:
function callNative() {
loadURL("your_func_name://xxx");
}
而后拿个 button 标签用一下就行了:
<button type="button" onclick="callNative()">Call Native!</button>
JavaScriptCore 方法
iOS 7 有了 JavaScriptCore 专门用来作 Native 与 JS 的交互。咱们能够在 webview 完成加载以后获取 JSContext,而后利用 JSContext 将 JS 中的对象引用过来用 Native 代码对其做出解释或响应:
// 首先引入 JavaScriptCore 库
#import <JavaScriptCore/JavaScriptCore.h>
// 而后再 UIWebView 的完成加载的代理方法中
- (void)webViewDidFinishLoad:(UIWebView *)webView {
// 获取 JS 上下文
jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
// 作引用,将 JS 内的元素引用过来解释,好比方法能够解释成 Block,对象也能够指向 OC 的 Native 对象哦
jsContext[@"iosDelegate"] = self;
jsContext[@"yourFuncName"] = ^(id parameter){
// 注意这里的线程默认是 web 处理的线程,若是涉及主线程操做须要手动转到主线程
dispatch_async(dispatch_get_main_queue(), ^{
// your code
});
}
}
而 JS 这边代码更简单了,干脆声明一个不解释的函数(约定好名字的),用于给 Native 作引用:
var parameter = xxx;
yourFuncName(parameter);
iOS Native 调用 JS
iOS Native 调用 JS 的实现方法也被 JavaScriptCore 划分开来:
webview 直接注入 JS 并执行
JavaScriptCore 方法
webview 直接注入 JS 并执行
在 iOS 平台,webview 有注入并执行 JS 的 API。
UIWebView
UIWebView 有直接注入 JS 的方法:
NSString *jsStr = [NSString stringWithFormat:@"showAlert('%@')", @"alert msg"];
[_webView stringByEvaluatingJavaScriptFromString:jsStr];
Note: 这个方法会返回运行 JS 的结果(nullable NSString *),它是一个同步方法,会阻塞当前线程!尽管此方法不被弃用,但最佳作法是使用 WKWebView 类的 evaluateJavaScript:completionHandler:method。
官方文档:
The stringByEvaluatingJavaScriptFromString: method waits synchronously for JavaScript evaluation to complete. If you load web content whose JavaScript code you have not vetted, invoking this method could hang your app. Best practice is to adopt the WKWebView class and use its evaluateJavaScript:completionHandler: method instead.
WKWebView
不一样于 UIWebView,WKWebView 注入并执行 JS 的方法不会阻塞当前线程。由于考虑到 webview 加载的 web content 内 JS 代码不必定通过验证,若是阻塞线程可能会挂起 App。
NSString *jsStr = [NSString stringWithFormat:@"setLocation('%@')", @"北京市东城区南锣鼓巷纳福胡同xx号"];
[_webview evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
NSLog(@"%@----%@", result, error);
}];
Note: 方法不会阻塞线程,并且它的回调代码块老是在主线程中运行。
官方文档:
Evaluates a JavaScript string.
The method sends the result of the script evaluation (or an error) to the completion handler. The completion handler always runs on the main thread.
JavaScriptCore 方法
上面简单提到过 JavaScriptCore 库提供的 JSValue 类,这里再提供一下官方文档对 JSValue 的介绍翻译:
JSValue 实例是对 JavaScript 值的引用。 您可使用 JSValue 类来转换 JavaScript 和 Objective-C 或 Swift 之间的基本值(如数字和字符串),以便在本机代码和 JavaScript 代码之间传递数据。
不过你也看到了我贴在上面的 OC 和 JS 数据类型转换表,那里面根本没有限定为官方文档所说的基本值。若是你不熟悉 JS 的话,我这里解释一下为何 JSValue 也能够指向 JS 中的对象和函数,由于 JS 语言不区分基本值和对象以及函数,在 JS 中“万物皆为对象”。
好了下面直接 show code:
// 首先引入 JavaScriptCore 库
#import <JavaScriptCore/JavaScriptCore.h>
// 先获取 JS 上下文
self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
// 若是涉及 UI 操做,切回主线程调用 JS 代码中的 YourFuncName,经过数组@[parameter] 入参
dispatch_async(dispatch_get_main_queue(), ^{
JSValue *jsValue = self.jsContext[@"YourFuncName"];
[jsValue callWithArguments:@[parameter]];
});
上面的代码调用了 JS 代码中 YourFuncName 函数,而且给函数加了 @[parameter] 做为入参。为了方便阅读理解,这里再贴一下 JS 代码:
function YourFuncName(arguments){
var result = arguments;
// do what u want to do
}
WKWebView 与 JS 交互的特有方法
wkwebview.jpg
关于 WKWebView 与 UIWebView 的区别就不在本文加以详细说明了,更多信息还请自行查阅。这里要讲的是 WKWebView 在与 JS 的交互时特有的方法:
WKUIDelegate 方法
MessageHandler 方法
WKUIDelegate 方法
对于 WKWebView 上文提到过,除了 WKNavigationDelegate,它还有一个 WKUIDelegate,这个 WKUIDelegate 是作什么用的呢?
WKUIDelegate 协议包含一些函数用来监听 web JS 想要显示 alert 或 confirm 时触发。咱们若是在 WKWebView 中加载一个 web 而且想要 web JS 的 alert 或 confirm 正常弹出,就须要实现对应的代理方法。
Note: 若是没有实现对应的代理方法,则 webview 将会按照默认操做去作出行为。
Alert: If you do not implement this method, the web view will behave as if the user selected the OK button.
Confirm: If you do not implement this method, the web view will behave as if the user selected the Cancel button.
咱们这里就拿 alert 举例,相信各位读者能够本身触类旁通。下面是在 WKUIDelegate 监听 web 要显示 alert 的代理方法中用 Native UIAlertController 替代 JS 中的 alert 显示的栗子 :
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
// 用 Native 的 UIAlertController 弹窗显示 JS 将要提示的信息
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提醒" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"知道了" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
// 函数内必须调用 completionHandler
completionHandler();
}]];
[self presentViewController:alert animated:YES completion:nil];
}
MessageHandler 方法
MessageHandler 是继 Native 截获 JS 假请求后另外一种 JS 调用 Native 的方法,该方法利用了 WKWebView 的新特性实现。对比截获假 Request 的方法来讲,MessageHandler 传参数更加简单方便。
MessageHandler 指什么?
WKUserContentController 类有一个方法:
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
该方法用来添加一个脚本处理器,能够在处理器内对 JS 脚本调用的方法作出处理,从而达到 JS 调用 Native 的目的。
那么 WKUserContentController 类和 WKWebView 有毛关系呢?
在 WKWebView 的初始化函数中有一个入参 configuration,它的类型是 WKWebViewConfiguration。WKWebViewConfiguration 中包含一个属性 userContentController,这个 userContentController 就是 WKUserContentController 类型的实例,咱们能够用这个 userContentController 来添加不一样名称的脚本处理器。
wkusercontentcontroller.jpg
MessageHandler 的坑
那么回到 - (void)addScriptMessageHandler:name: 方法上面,该方法添加一个脚本消息处理器(第一个入参 scriptMessageHandler),而且给这个处理器起一个名字(第二个入参 name)。不过这个函数在使用的时候有个坑:scriptMessageHandler 入参会被强引用,那么若是你把当前 WKWebView 所在的 UIViewController 做为第一个入参,这个 viewController 被他本身所持有的 webview.configuration. userContentController 所持有,就会形成循环引用。
retaincycle.jpg
咱们能够经过 - (void)removeScriptMessageHandlerForName: 方法删掉 userContentController 对 viewController 的强引用。因此通常状况下咱们的代码会在 viewWillAppear 和 viewWillDisappear 成对儿的添加和删除 MessageHandler:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.webview.configuration.userContentController addScriptMessageHandler:self name:@"YourFuncName"];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.webview.configuration.userContentController removeScriptMessageHandlerForName:@"YourFuncName"];
}
WKScriptMessageHandler 协议
WKScriptMessageHandler 是脚本信息处理器协议,若是想让一个对象具备脚本信息处理能力(好比上文中 webview 的所属 viewController 也就是上面代码的 self)就必须使其遵循该协议。
WKScriptMessageHandler 协议内部很是简单,只有一个方法,咱们必需要实现该方法(@required):
// WKScriptMessageHandler 协议方法,在接收到脚本信息时触发
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
// message 有两个属性:name 和 body
// message.name 能够用于区别要作的处理
if ([message.name isEqualToString:@"YourFuncName"]) {
// message.body 至关于 JS 传递过来的参数
NSLog(@"JS call native success %@", message.body);
}
}
补充 JS 的代码:
// <name> 换 YourFuncName,<messageBody> 换你要的入参便可
window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
本文转自:http://www.jianshu.com/p/5329170be7b3