iOS中JS与OC相互调用的方式

Web App即是HTML5 APP框架开发模式,使用HTML5,CSS3以及JavaScript以及服务器端语言来完成开发,Web App具有跨平台的优势。那么就来学习一下iOS中JS与OC相互调用的方式。

目前JS与OC相互调用的方式

目前主要的JS与OC相互调用方式主要有如下6种:

  • 在JS 中做一次URL跳转,然后在OC中拦截跳转。(这里分为UIWebView 和 WKWebView两种,UIWebView兼容iOS 6)
  • 利用WKWebView 的MessageHandler。(有坑)
  • 利用系统库JavaScriptCore,来做相互调用。(iOS 7推出)
  • 利用第三方库WebViewJavascriptBridge。
  • 利用第三方cordova库,以前叫PhoneGap。(这是一个库平台的库)
  • 当下盛行的React Native。

拦截URL

UIWebView

在以前我们只能通过UIWebView的UIWebViewDelegate协议来实现oc与js的通信交互就是发送消息,也即函数调用。

1
2
3
4
5
6
7
8
9
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

@protocol UIWebViewDelegate <NSObject>

@optional
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
- (void)webViewDidStartLoad:(UIWebView *)webView;
- (void)webViewDidFinishLoad:(UIWebView *)webView;
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error;
  • oc–>js stringByEvaluatingJavaScriptFromString,其参数是一NSString 字符串内容是js代码(这又可以是一个js函数、一句js代码或他们的组合),当js函数有返回值或一句js代码有值返回可通过stringByEvaluatingJavaScriptFromString的返回值获取
  • js–>oc 利用webView的重定向原理(即重新在js中指定document.location的值,此为一url),只要在这个url字符串中按自定义的规则指定好所需调用oc中的函数和参数,然后通过OC中的shouldStartLoadWithRequest函数去捕获处理请求。

WKWebView

由于UIWebView比较耗内存,性能上不太好,而苹果在iOS8中推出了WKWebView。同样的用WKWebView也可以拦截URL,做JS与OC交互。WKWebView与UIWebView拦截URL的处理方式基本一样。除了代理方法和WKWebView的使用不太一样。

优点

更多的支持HTML5的特性
官方宣称的高达60fps的滚动刷新率以及内置手势
将UIWebViewDelegate与UIWebView拆分成了14类与3个协议,以前很多不方便实现的功能得以实现。文档
Safari相同的JavaScript引擎
占用更少的内存

使用

WKWebView的创建有几点不同:

  • 初始化多了configuration参数,当然这个参数也可以不传,直接使用默认的设置。
  • WKWebView的代理有两个navigationDelegate和UIDelegate。我们要拦截URL,就要通过navigationDelegate的一个代理方法来实现。如果在HTML中要使用alert等弹窗,就必须得实现UIDelegate的相应代理方法。
  • 在iOS9之前,WKWebView加载本地HTML会有一些问题。(不能加载本地HTML,或者部分CSS/本地图片加载不了等)
  • 注意加载HTTP和HTTPS的时候要在文件设置App Transport Security Settings的Allow Arbitrary Loads设置为YES。

WKNavigationDelegate

1
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler

实现这个代理方法,必须得调用decisionHandler这个block,否则会导致app 崩溃。block参数是个枚举类型,WKNavigationActionPolicyCancel代表取消加载,相当于UIWebView的代理方法return NO的情况;WKNavigationActionPolicyAllow代表允许加载,相当于UIWebView的代理方法中return YES的情况。

WKWebView中OC直接调用JS方法

1
2
3
4
5
NSString *js = @"callJsAlert()";
[self.webView evaluateJavaScript:js completionHandler:^(id _Nullable response, NSError * _Nullable error) {
NSLog(@"response: %@ error: %@", response, error);
NSLog(@"call js alert by native");
}];

evaluateJavaScript:completionHandler:没有返回值,JS 执行成功还是失败会在completionHandler 中返回。所以使用这个API 就可以避免执行耗时的JS,或者alert 导致界面卡住的问题。

WKWebView中JS调用OC原生方法

MessageHandler

利用WKWebView的新特性MessageHandler来实现JS调用原生方法。

MessageHandler的好处

  • 在JS中写起来简单,不用再用创建URL的方式那么麻烦了。
  • JS传递参数更方便。使用拦截URL的方式传递参数,只能把参数拼接在后面,如果遇到要传递的参数中有特殊字符,如&、=、?等,必须得转换,否则参数解析肯定会出错。
  • 使用MessageHandler就可以避免特殊字符引起的问题

实现原理

WKWebView初始化时,有一个参数叫configuration,它是WKWebViewConfiguration类型的参数,而WKWebViewConfiguration有一个属性叫userContentController,它又是WKUserContentController类型的参数。WKUserContentController对象有一个方法- addScriptMessageHandler:name:,我把这个功能简称为MessageHandler。

- addScriptMessageHandler:name:有两个参数,第一个参数是userContentController的代理对象,第二个参数是JS里发送postMessage的对象。

所以要使用MessageHandler功能,就必须要实现WKScriptMessageHandler协议。
在OC文件中:

1
2
3
4
5
6
7
8
9
10
11
12
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
config.userContentController = [[WKUserContentController alloc] init];
// 注入JS对象名称AppModel,当JS通过AppModel来调用时,我们可以在WKScriptMessageHandler代理中接收到
[config.userContentController addScriptMessageHandler:self name:@"AppModel"];

//WKScriptMessageHandler协议
- (void)userContentController:(WKUserContentController *)userContentController
didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"AppModel"]) {
NSLog(@"%@", message.body);
}
}

实现协议方法。
这里实现了两个协议< WKUIDelegate,WKScriptMessageHandler >,WKUIDelegate是因为需要在JS中弹出alert。WKScriptMessageHandler是用来处理JS调用OC方法的请求。名字替换AppModel

在JS文件中:

1
2
// AppModel是我们所注入的对象
window.webkit.messageHandlers.AppModel.postMessage({body: response});

JavaScriptCore

JavaScriptCore自iOS7之后出现,用Objective-C把WebKit的JavaScript引擎封装了一下,提供了简单快捷的方式与JavaScript交互。

特点:

  • JS中调用OC方法更简单,参数的传递也更加简单
  • 支持iOS7以上

使用

使用前需要先导入JavaScriptCore.framework

  • JSVirtualMachine直译是JS虚拟机,也就是说JavaScript是在一个虚拟的环境中执行,而JSVirtualMachine为其执行提供底层资源。
  • JSContext是为JavaScript的执行提供运行环境,所有的JavaScript的执行都必须在JSContext环境中。JSContext也管理JSVirtualMachine中对象的生命周期。每一个JSValue对象都要强引用关联一个JSContext。当与某JSContext对象关联的所有JSValue释放后,JSContext也会被释放。
  • JSValue都是通过JSContext返回或者创建的,并没有构造方法。JSValue包含了每一个JavaScript类型的值,通过JSValue可以将Objective-C中的类型转换为JavaScript中的类型,也可以将JavaScript中的类型转换为Objective-C中的类型。
  • JSManagedValue主要用途是解决JSValue对象在Objective-C堆上的安全引用问题。把JSValue保存进Objective-C堆对象中是不正确的,这很容易引发循环引用,而导致JSContext不能释放。但是不常用。
  • JSExport是一个协议类,但是该协议并没有任何属性和方法。可以自定义一个协议类,继承自JSExport。无论在JSExport里声明的属性、实例方法还是类方法,继承的协议都会自动的提供给任何JavaScript代码。因此,只需要在自定义的协议类中,添加上属性和方法就可以了。

WKWebVIew中的坑

WKWebView导致ViewController不调用dealloc方法

  • webview不设置 delegate,可以正常dealloc
  • webview.UIDelegate和 navigationDelegate 设置为 self,可以正常dealloc
  • webview.configuration.userContentController addScriptMessageHandler 为 self之后,无法正常dealloc,该方法引起ViewController内存泄漏。
  • 使用 ScriptMessageHandler 作为webview.scriptMessageHandler,当前viewController 可以正常释放。
  • 当前 viewController dealloc 方法中,通过 removeScriptMessazgeHanlder 释放掉
    scriptMessageHandler

    解决方法:
    (1)WeakScriptMessageDelegate
    可以创建一个新的类WeakScriptMessageDelegate,也可以将@interface-@end写在ViewController.h中,@implementation-@end写在ViewController.m中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@interface WeakScriptMessageDelegate : NSObject<WKScriptMessageHandler>

@property (nonatomic, weak) id<WKScriptMessageHandler> scriptDelegate;

- (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate;

@end
@implementation WeakScriptMessageDelegate

- (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate {
self = [super init];
if (self) {
_scriptDelegate = scriptDelegate;
}
return self;
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
[self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message];
}

@end

(2)使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
_webView.UIDelegate = self;
_webView.navigationDelegate = self;

WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
configuration.userContentController = [WKUserContentController new];

//无法正常dealloc[configuration.userContentController addScriptMessageHandler:self name:@"myHandler"];
//使用代理来实现可以调用dealloc
[configuration.userContentController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"myHandler"];

- (void)dealloc {
[self.userContentController removeScriptMessageHandlerForName:@"myHandler"];
}

Demo地址:https://github.com/peilinghui/BokeDemo/tree/master/PLHWebViewDemo

参考

WKWebView使用及注意点(keng)

扩展

使用 JavaScriptCore 实现 JS和OC间的通信

使用safari对webview进行调试

文章目录
  1. 1. 目前JS与OC相互调用的方式
  2. 2. 拦截URL
    1. 2.1. UIWebView
    2. 2.2. WKWebView
      1. 2.2.1. 优点
      2. 2.2.2. 使用
        1. 2.2.2.1. WKNavigationDelegate
    3. 2.3. MessageHandler
      1. 2.3.1. MessageHandler的好处
      2. 2.3.2. 实现原理
    4. 2.4. JavaScriptCore
      1. 2.4.1. 使用
  3. 3. WKWebVIew中的坑
    1. 3.1. WKWebView导致ViewController不调用dealloc方法
  4. 4. 参考
  5. 5. 扩展
本站总访问量 本站访客数人次 ,