参考
- iOS WKWebView 与 JS 交互实战技巧
- iOS进阶之WKWebView
一、JS调用OC
什么是 MessageHandler
一句话总结:OC中注册方法给JS调用。
OC和JS注册相同的方法名,OC添加
MessageHandler
来监听JS的事件并回调,即:JS调用OC。
下面分析
1)WKWebView
有这样一个属性:
@property (nonatomic, readonly, copy) WKWebViewConfiguration *configuration;
可以看到configuration
是readonly
。
2)所以只能通过WKWebView
的初始化方法去配置:
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;
3)WKWebViewConfiguration
又是何方神圣?其实就是WKWebView
的配置类,在这里,我们的目的是注册方法给JS调用,那么对应着,WKWebViewConfiguration
的属性userContentController
就是用来实现这个目的,称之为MessageHandler
。关于WKWebViewConfiguration
的其它属性,我们用到的时候再做分析。
@property (nonatomic, strong) WKUserContentController *userContentController;
查看WKUserContentController
,可以看到有如下的方法,基本上add
和remove
方法是成对出现的,虐狗啊:
最终,为webview.configuration.userContentController
注册JS同名方法sayHello
:
[_userContentController addScriptMessageHandler:self name:@"sayHello"];
并设置WKScriptMessageHandler
来回调:
#pragma mark -WKScriptMessageHandler
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
}
具体看下完整代码:
WKWebViewController.h
#import
@interface WKWebViewController : UIViewController
@end
WKWebViewController.m
#import
#import
@interface WKWebViewController (){
}
@property(nonatomic,strong)WKWebView *webView;
@property(nonatomic,strong)WKUserContentController *userContentController;
@end
@implementation WKWebViewController
-(void)dealloc{
NSLog(@"dealloc run");
[_userContentController removeScriptMessageHandlerForName:@"sayHello"];
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.webView.navigationDelegate = self;
self.webView.UIDelegate = self;
[self loadMyHtmlPage];
}
-(WKWebView*)webView{
if (!_webView) {
//初始化WKWebView,以及配置MessageHandler
_userContentController = [[WKUserContentController alloc]init];
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc]init];
configuration.userContentController = _userContentController;
_webView = [[WKWebView alloc]initWithFrame:self.view.frame configuration:configuration];
[self.view addSubview:_webView];
[_webView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.view);
}];
//注册方法给JS调用
[_userContentController addScriptMessageHandler:self name:@"sayHello"];
}
return _webView;
}
-(void)loadMyHtmlPage{
NSString *bundlePath = [[NSBundle mainBundle]pathForResource:@"MyHtml" ofType:@"html"];
// NSString *filePath = [NSString stringWithContentsOfFile:bundlePath encoding:NSUTF8StringEncoding error:nil];
// NSURL *baseUrl = [NSURL fileURLWithPath:filePath];
// [self.webView loadHTMLString:filePath baseURL:baseUrl];
NSURL *fileURL = [NSURL fileURLWithPath:bundlePath];
[self.webView loadFileURL:fileURL allowingReadAccessToURL:fileURL];
}
#pragma mark -WKUIDelegate
#pragma mark -WKNavigationDelegate
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
NSLog(@"%@",navigationResponse.response.URL.absoluteString);
//允许跳转
decisionHandler(WKNavigationResponsePolicyAllow);
//不允许跳转
//decisionHandler(WKNavigationResponsePolicyCancel);
}
#pragma mark -WKScriptMessageHandler
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
//根据名字
if ([message.name isEqualToString:@"sayHello"]) {
NSLog(@"name:%@\n body:%@\n frameInfo:%@\n",message.name,message.body,message.frameInfo);
}
}
@end
可以看到,add
和remove
messageHandler
是成对出现的:
[_userContentController addScriptMessageHandler:self name:@"sayHello"];
[_userContentController removeScriptMessageHandlerForName:@"sayHello"];
对应的H5代码MyHtml.html
如下:
this is test page
其中,关键的是say()
方法中的这句写法,注册一个与OC同名注册的方法sayHello
,传递的参数是一个字典:{body:'hello world'}
window.webkit.messageHandlers.sayHello.postMessage({body:'hello world'});
这一句的写法,可以在WKUserContentController
类中找到:
PS:如果window.webkit.messageHandlers.sayHello.postMessage({body:'hello world'});
这一句前端的H5报错,可以让前端的人员关闭代码校验功能,访问该页面时,会自动注入协议的(网上人这么说的)
可能遇到的问题
1)循环引用造成无法removeScriptMessageHandler,造成内存泄漏
假如,WKWebViewController
是在二级以上的页面,以上代码,你会发现pop
或者dismiss
这个页面的时候,dealloc
方法不会调用,也就是MessageHandler
无法remove
,这样会造成内存泄漏。为什么会这样的?经过分析,下图应该一目了然:
所以最终是[_userContentController addScriptMessageHandler:self name:@"sayHello"];
这一句引起的循环引用,导致控制器无法被释放,所以UIViewController
的dealloc
才不会调用,从而引起内存泄露。解决的方式大致可以有两种:
- 将
MessageHandle
的remove
移到其他方法中,例如viewWillDisappear
。
但是这种方式,不能满足所有的业务需求,如果你是想要在viewcontroller移除,也就是
dealloc
的时候,再移除对应的MessageHandle
,而不希望在present或者push的情况下也移除对应的MessageHandle
。那么只能先解决循环引用的问题,再进行MessageHandle
的移除,而不是移除MessageHandle
从而解决对应的循环引用,二者的因果关系是相反的!最终的目的都是为了解决内存泄露,方法如下:
- 写一个新的
controller
,用新的controller
的delegate
作为跳板绕回来,这是不是叫做协议转发呢?(知道的大神告知一声,感谢)
具体如下图:
新的controller
就叫WXX_WKUserContentController
具体的写法如下:
WXX_WKUserContentController.h
#import
#import
@protocol WXX_WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
@end
@interface WXX_WKUserContentController : UIViewController
@property(weak,nonatomic)iddelegate;
@end
WXX_WKUserContentController.m
#import "WXX_WKUserContentController.h"
@interface WXX_WKUserContentController ()
@end
@implementation WXX_WKUserContentController
- (void)viewDidLoad {
[super viewDidLoad];
}
#pragma mark -WKScriptMessageHandler
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
if ([self.delegate respondsToSelector:@selector(userContentController:didReceiveScriptMessage:)]) {
[self.delegate userContentController:userContentController didReceiveScriptMessage:message];
}
}
@end
WKWebViewController.m
中则做相应的修改:
#import "WKWebViewController.h"
#import
#import
#import "WXX_WKUserContentController.h"
@interface WKWebViewController (){
}
@property(nonatomic,strong)WKWebView *webView;
@property(nonatomic,strong)WKUserContentController *userContentController;
@end
@implementation WKWebViewController
-(void)dealloc{
NSLog(@"dealloc run");
[_userContentController removeScriptMessageHandlerForName:@"sayHello"];
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.webView.navigationDelegate = self;
self.webView.UIDelegate = self;
[self loadMyHtmlPage];
}
-(WKWebView*)webView{
if (!_webView) {
//初始化WKWebView,以及配置MessageHandler
_userContentController = [[WKUserContentController alloc]init];
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc]init];
configuration.userContentController = _userContentController;
_webView = [[WKWebView alloc]initWithFrame:self.view.frame configuration:configuration];
[self.view addSubview:_webView];
[_webView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.view);
}];
//注册方法给JS调用
WXX_WKUserContentController *delegateController = [[WXX_WKUserContentController alloc]init];
delegateController.delegate = self;
[_userContentController addScriptMessageHandler:delegateController name:@"sayHello"];
}
return _webView;
}
-(void)loadMyHtmlPage{
NSString *bundlePath = [[NSBundle mainBundle]pathForResource:@"MyHtml" ofType:@"html"];
// NSString *filePath = [NSString stringWithContentsOfFile:bundlePath encoding:NSUTF8StringEncoding error:nil];
// NSURL *baseUrl = [NSURL fileURLWithPath:filePath];
// [self.webView loadHTMLString:filePath baseURL:baseUrl];
NSURL *fileURL = [NSURL fileURLWithPath:bundlePath];
[self.webView loadFileURL:fileURL allowingReadAccessToURL:fileURL];
}
#pragma mark -WKUIDelegate
#pragma mark -WKNavigationDelegate
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
NSLog(@"%@",navigationResponse.response.URL.absoluteString);
//允许跳转
decisionHandler(WKNavigationResponsePolicyAllow);
//不允许跳转
//decisionHandler(WKNavigationResponsePolicyCancel);
}
#pragma mark -WKScriptMessageHandler
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
//根据名字
if ([message.name isEqualToString:@"sayHello"]) {
NSLog(@"name:%@\n body:%@\n frameInfo:%@\n",message.name,message.body,message.frameInfo);
}
}
@end
2)加载html的方式不对,造成OC和JS无法交互
使用这种方式加载HTML,无法交互:
NSString *bundlePath = [[NSBundle mainBundle]pathForResource:@"MyHtml" ofType:@"html"];
NSString *filePath = [NSString stringWithContentsOfFile:bundlePath encoding:NSUTF8StringEncoding error:nil];
NSURL *baseUrl = [NSURL fileURLWithPath:filePath];
[self.webView loadHTMLString:filePath baseURL:baseUrl];
换成这种就又可以了:
NSString *bundlePath = [[NSBundle mainBundle]pathForResource:@"MyHtml" ofType:@"html"];
NSURL *fileURL = [NSURL fileURLWithPath:bundlePath];
[self.webView loadFileURL:fileURL allowingReadAccessToURL:fileURL];
需要去了解下不同的加载方式,有大神指导的请告知下。
二、OC调用JS
MyHtml.html:
this is test page
调用:
-(void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{
//js_method()是JS的方法名,并异步回调返回了数据:result
[webView evaluateJavaScript:@"js_method()" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
NSLog(@"OC调用JS的方法:js_method() ---- %@",result);
}];
[webView evaluateJavaScript:@"document.title" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
NSLog(@"document.title == %@",result);
}];
}
结果:
OC调用JS的方法:js_method() ---- this is js method return value
document.title == this is test page
三、WebViewJavascriptBridge
WebViewJavascriptBridge是对原生的OC和JS交互的一个框架,同时支持WKWebViews
, UIWebViews
和WebViews
具体的使用,之后另开一篇。
总结:
addScriptMessageHandler
引起的循环引用,导致viewcontroller
不能正常dealloc
,写在dealloc
中的removeScriptMessageHandlerForName
也就不能正常调用,从而引起内存泄漏。具体的两种解决方式:
1)在viewWillDisappear
中调用removeScriptMessageHandlerForName
,解除了循环引用并且能够正常remove
。
2)通过一个中介性质的viewcontroller
的delegate
作为跳板,解除循环引用,从而正常执行dealloc
并正常remove
。关于OC和JS的传值,只能传一个参数,因此如果多个参数,可以通过字典或者json来作为参数。