中国领先的IT技术网站
|
|

iOS Native与JavaScript交互

说到 Native 与 JS 做交互,就不得不提一嘴 HyBird Mobile App。Hybird 的翻译结果并不是很文明(擦汗,不知道为啥很多翻译软件会译为“杂种”,但我更喜欢将它翻译为“混合、混血”),Hybird App 我对它的理解为通过 Web 网络技术(如 HTML,CSS 和 JavaScript)与 Native 相结合的混合移动应用程序。

作者:佚名来源:Lision|2017-10-18 12:22

Tech Neo技术沙龙 | 11月25号,九州云/ZStack与您一起探讨云时代网络边界管理实践


说到 Native 与 JS 做交互,就不得不提一嘴 HyBird Mobile App。

Hybird 的翻译结果并不是很文明(擦汗,不知道为啥很多翻译软件会译为“杂种”,但我更喜欢将它翻译为“混合、混血”),Hybird App 我对它的理解为通过 Web 网络技术(如 HTML,CSS 和 JavaScript)与 Native 相结合的混合移动应用程序。

那么我们来看一下 Hybird 对比 Native 有哪些优劣:

iOS Native与JavaScript交互

因为 Hybird 的灵活性(更改 Web 页面内的 JS 就可以直接生效不必重新发版)以及通用性(一份 H5 玩遍公众号、Android、iOS)再加上门槛低(前端玩家过来可以无痛上手开撸)的优势,所以在非核心功能模块使用 Web 通过 Hybird 的方式实现可能从各方面都会稍微好一些。而 Native 则可以在核心功能和设备硬件的调用上为 JS 提供强有力的支持。

虽然现在很多技术如 RN 和 Weex 等通过 JS 直接写 Native 的技术出现,对 Hybird App 冲击很大。但是由于 Hybird 技术门槛低(几乎无学习成本)等原因,国内很多公司包括大厂依旧没有完全抛弃它,毕竟合适的才是最好的!

Hybird 的发展史

H5 发布

Html5 是在 2014 年 9 月份正式发布的,这一次的发布做了一个最大的改变就是“从以前的 XML 子集升级成为一个独立集合”。

iOS Native与JavaScript交互

H5 渗入 Mobile App 开发

Native APP 开发中有一个 webview 的组件(Android 中是 webview,iOS 有 UIWebview和 WKWebview),这个组件可以加载 Html 文件。

在 H5 大行其道之前,webview 加载的 web 页面单调(因为只能加载一些静态资源)。可自打 H5 火了之后,在很多仿生框架的帮助下,前端玩家们开发的 H5 页面在 webview 中的体验不俗使得 H5 开发慢慢渗透到了 Mobile App 开发中来。

Hybird 的现状

虽然目前已经出现了 RN 和 Weex 这些 JS 写 Native 的技术,但是 Hybird 依旧没有被淘汰。市面上绝大多说应用都不同程度的引用了 Web 页面,Web 页面中的 JS 与 Native 如何交互依然是每个 iOS 猿必须掌握的技能。

JavaScriptCore

不好意思,在进入正题之前,容我再 BB 一下。JavaScriptCore 这个库是 Apple 在 iOS 7 之后加入的,它对 iOS Native 与 JS 做交互调用产生了划时代的影响。

JavaScriptCore 大体是由 4 个类以及 1 个协议组成的:

iOS Native与JavaScript交互

  • JSContext 是 JS 执行上下文,你可以把它理解为 JS 运行的环境
  • JSValue 是对 JavaScript 值的引用,任何 JS 中的值都可以被包装为一个 JSValue
  • JSManagedValue 是对 JSValue 的包装,加入了“conditional retain”
  • JSVirtualMachine 表示 JavaScript 执行的独立环境

还有 JSExport 协议:

实现将 Objective-C 类及其实例方法,类方法和属性导出为 JavaScript 代码的协议。

这里的 JSContext,JSValue,JSManagedValue 相对比较好理解,下面我们把 JSVirtualMachine 单拎出来说明一下:

JSVirtualMachine 的用法和其与 JSContext 的关系

iOS Native与JavaScript交互

官方文档的介绍:

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与JavaScript交互

iOS 与 JS 交互

对于 iOS Native 与 JS 做交互我们先从调用方向上分为两种情况来看:

  • JS 调用 iOS Native
  • iOS Native 调用 JS

iOS Native与JavaScript交互

JS 调用 iOS Native

其实 JS 调用 iOS Native 也分为两种实现方式:

  • 假 Request 方法
  • JavaScriptCore 方法

假 Request 方法

原理:其实这种方式就是利用了 webview 的代理方法,在 webview 开始请求的时候截获请求,判断请求是否为约定好的假请求。如果是假请求则表示是 JS 想要按照约定调用我们的 Native 方法,按照约定去执行我们的 Native 代码就好。

UIWebView

UIWebView 代理用于截获请求的代理函数,在里面做判断就好:

  1. - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { 
  2.     NSURL *url = request.URL; 
  3.     // 与约定好的函数名作比较 
  4.     if ([[url scheme] isEqualToString:@"your_func_name"]) { 
  5.         // just do it 
  6.     } 

WKWebView

WKWebView 有两个代理,一个是 WKNavigationDelegate,另一个是 WKUIDelegate。我们需要设置并实现它的 WKNavigationDelegate 方法:

  1. - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { 
  2.     NSURL *url = navigationAction.request.URL; 
  3.     // 与约定好的函数名作比较 
  4.     if ([[url scheme] isEqualToString:@"your_func_name"]) { 
  5.         // just do it 
  6.         decisionHandler(WKNavigationActionPolicyCancel); 
  7.         return
  8.     } 
  9.      
  10.     decisionHandler(WKNavigationActionPolicyAllow); 

Note: decisionHandler 当你的应用程序决定是允许还是取消导航时,要调用的块。 该块使用单个参数,它必须是枚举类型 WKNavigationActionPolicy 的常量之一。如果不调用 decisionHandler 会引起 crash。

这里补充一下 JS 段的代码:

  1. function callNative(){ 
  2.     loadURL("your_func_name://xxx"); 

然后拿个 button 标签用一下就好了:

  1. <button type="button" onclick="callNative()">Call Native!</button> 

Call Native!

其实这里有个实际的栗子来的,我之前写过一篇文章,由于适配原因采取了假 Request 的方式也做到了解决问题。文章链接贴在这里 各位想看的可以移步去看一下哈。

JavaScriptCore 方法

iOS 7 有了 JavaScriptCore 专门用来做 iOS Native 与 JS 的交互。我们可以在 webview 完成加载之后获取 JSContext,然后利用 JSContext 将 JS 中的对象引用过来用 Native 代码对其作出响应:

  1. // 首先引入 JavaScriptCore 库 
  2. #import<JavaScriptCore/JavaScriptCore.h> 
  3.  
  4. // 然后再 UIWebView 的完成加载的代理方法中 
  5. - (void)webViewDidFinishLoad:(UIWebView *)webView { 
  6.     // 获取 JS 上下文 
  7.     jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; 
  8.     // 做引用,将 JS 内的元素引用过来解释,比如方法可以解释成 Block,对象也可以指向 OC 的 Native 对象哦 
  9.     jsContext[@"iosDelegate"] = self; 
  10.     jsContext[@"yourFuncName"] = ^(id parameter){ 
  11.         // 注意这里的线程默认是 web 处理的线程,如果涉及主线程操作需要手动转到主线程 
  12.         dispatch_async(dispatch_get_main_queue(), ^{ 
  13.         // your code 
  14.         }); 
  15.     } 

而 JS 这边代码更简单了,干脆声明一个不解释的函数(约定好名字的),用于给 Native 做引用:

  1. var parameter = xxx;  
  2. yourFuncName(parameter); 

iOS Native 调用 JS

iOS Native 调用 JS 的实现方法也被 JavaScriptCore 划分开来:

  • webview 直接注入 JS 并执行
  • JavaScriptCore 方法

webview 直接注入 JS 并执行

  • 在 iOS 平台,webview 有注入并执行 JS 的 API。

UIWebView

UIWebView 有直接注入 JS 的方法:

  1. NSString *jsStr = [NSString stringWithFormat:@"showAlert('%@')", @"alert msg"];  
  2. [_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。

  1. NSString *jsStr = [NSString stringWithFormat:@"setLocation('%@')", @"北京市东城区南锣鼓巷纳福胡同xx号"]; 
  2. [_webview evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) { 
  3.     NSLog(@"%@----%@", result, error); 
  4. }]; 

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:

  1. // 首先引入 JavaScriptCore 库 
  2. #import<JavaScriptCore/JavaScriptCore.h> 
  3.  
  4. // 先获取 JS 上下文 
  5. self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; 
  6. // 如果涉及 UI 操作,切回主线程调用 JS 代码中的 YourFuncName,通过数组@[parameter] 入参 
  7. dispatch_async(dispatch_get_main_queue(), ^{ 
  8.     JSValue *jsValue = self.jsContext[@"YourFuncName"]; 
  9.     [jsValue callWithArguments:@[parameter]]; 
  10. }); 

上面的代码调用了 JS 代码中 YourFuncName 函数,并且给函数加了 @[parameter] 作为入参。为了方便阅读,这里再补充一下 JS 代码:

  1. function YourFuncName(arguments){ 
  2.     var result = arguments; 
  3.     // do what u want to do 

WKWebView 与 JS 交互的特有方法

iOS Native与JavaScript交互

关于 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 显示的栗子 :

  1. - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { 
  2.     // 用 Native 的 UIAlertController 弹窗显示 JS 将要提示的信息 
  3.     UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提醒" message:message preferredStyle:UIAlertControllerStyleAlert]; 
  4.     [alert addAction:[UIAlertAction actionWithTitle:@"知道了" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { 
  5.         // 函数内必须调用 completionHandler 
  6.         completionHandler(); 
  7.     }]]; 
  8.      
  9.     [self presentViewController:alert animated:YES completion:nil]; 

MessageHandler 方法

MessageHandler 是继 Native 截获 JS 假请求后另一种 JS 调用 Native 的方法,该方法利用了 WKWebView 的新特性实现。对比截获假 Request 的方法来说,MessageHandler 传参数更加简单方便。

MessageHandler 指什么?

WKUserContentController 类有一个方法:

  1. - (void)addScriptMessageHandler:(id )scriptMessageHandler name:(NSString *)name

该方法用来添加一个脚本处理器,可以在处理器内对 JS 脚本调用的方法做出处理,从而达到 JS 调用 Native 的目的。

那么 WKUserContentController 类和 WKWebView 有毛关系呢?

在 WKWebView 的初始化函数中有一个入参 configuration,它的类型是 WKWebViewConfiguration。WKWebViewConfiguration 中包含一个属性 userContentController,这个 userContentController 就是 WKUserContentController 类型的实例,我们可以用这个 userContentController 来添加不同名称的脚本处理器。

iOS Native与JavaScript交互

MessageHandler 的坑

那么回到 - (void)addScriptMessageHandler:name: 方法上面,该方法添加一个脚本消息处理器(第一个入参 scriptMessageHandler),并且给这个处理器起一个名字(第二个入参 name)。不过这个函数在使用的时候有个坑:scriptMessageHandler 入参会被强引用,那么如果你把当前 WKWebView 所在的 UIViewController 作为第一个入参,这个 viewController 被他自己所持有的 webview.configuration. userContentController 所持有,就会造成循环引用。

iOS Native与JavaScript交互

一般我们通过 - (void)removeScriptMessageHandlerForName: 方法删掉 userContentController 对 viewController 的强引用。所以一般情况下我们的代码会在 viewWillAppear 和 viewWillDisappear 成对儿的添加和删除 MessageHandler:

  1. - (void)viewWillAppear:(BOOL)animated { 
  2.     [super viewWillAppear:animated]; 
  3.     [self.webview.configuration.userContentController addScriptMessageHandler:self name:@"YourFuncName"]; 
  4.  
  5. - (void)viewWillDisappear:(BOOL)animated { 
  6.     [super viewWillDisappear:animated]; 
  7.     [self.webview.configuration.userContentController removeScriptMessageHandlerForName:@"YourFuncName"]; 

WKScriptMessageHandler 协议

WKScriptMessageHandler 是脚本信息处理器协议,如果想让一个对象具有脚本信息处理能力(比如上文中 webview 的所属 viewController 也就是上面代码的 self)就必须使其遵循该协议。

WKScriptMessageHandler 内部简单的一匹,只有一个方法,我们必须要实现该方法(@required):

  1. // WKScriptMessageHandler 协议方法,在接收到脚本信息时触发 
  2. - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { 
  3.     // message 有两个属性:name 和 body 
  4.     // message.name 可以用于区别要做的处理 
  5.     if ([message.name isEqualToString:@"YourFuncName"]) { 
  6.         // message.body 相当于 JS 传递过来的参数 
  7.         NSLog(@"JS call native success %@", message.body); 
  8.     } 

老样子,补充一下 JS 的代码:

  1. // <name> 换 YourFuncName,<messageBody> 换你要的入参即可 
  2. window.webkit.messageHandlers.<name>.postMessage(<messageBody>) 

搞定收工!

总结

  • 这篇文章简单的介绍了一下 Hybird Mobile App(其中还包括 Hybird 的发展简史)。
  • 介绍了 JavaScriptCore 的组成,并且把很多文章没有讲清楚的 JSVirtualMachine 与 JSContext 和 JSValue 之间的关系用图片的形式表述出来。(JSVirtualMachine 包含 JSContext 包含 JSValue,都是 1 对 n 的关系,且由于同一个 JSVirtualMachine 下的代码会相互阻塞,所以如果想异步执行交互需要在不同的线程声明 JSVirtualMachine 并发执行)
  • 从调用方向的角度把 JS 与 iOS Native 相互调用的方式方法分别用代码讲解了一遍。
  • 最后补充了 WKWebView 与 JS 交互特有的方法:WKUIDelegate 和 MessageHandler。

如果您觉得我的文章表述错误请您予以指正,您的指正将会让我避免误导很多人。

【编辑推荐】

  1. 编写可测试的JavaScript代码
  2. 史上最详细的Android原生APP中添加ReactNative进行混合开发教程
  3. React Native中ScrollView性能探究
  4. React Native触摸事件处理详解
  5. 9款极佳的JavaScript移动应用程序开发框架
【责任编辑:未丽燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

网络工程师必读——接入网与交换网

本书是以一个典型的计算机广域网通信为背景进行编写的,详细、全面地介绍了通信网中最主要的两个部分:目前,国内外接入网与交换网的主要技...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊
× CTO训练营(深圳站)