OC和JS交互(二):WKWebView之MessageHandler

参考

  • 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;

可以看到configurationreadonly

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,可以看到有如下的方法,基本上addremove方法是成对出现的,虐狗啊:

OC和JS交互(二):WKWebView之MessageHandler_第1张图片
WKUserContentController的方法.png

最终,为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

可以看到,addremove 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类中找到:

OC和JS交互(二):WKWebView之MessageHandler_第2张图片
postMessage.png

PS:如果window.webkit.messageHandlers.sayHello.postMessage({body:'hello world'});这一句前端的H5报错,可以让前端的人员关闭代码校验功能,访问该页面时,会自动注入协议的(网上人这么说的)

可能遇到的问题

1)循环引用造成无法removeScriptMessageHandler,造成内存泄漏

假如,WKWebViewController是在二级以上的页面,以上代码,你会发现pop或者dismiss这个页面的时候,dealloc方法不会调用,也就是MessageHandler无法remove,这样会造成内存泄漏。为什么会这样的?经过分析,下图应该一目了然:

OC和JS交互(二):WKWebView之MessageHandler_第3张图片
循环引用.png

所以最终是[_userContentController addScriptMessageHandler:self name:@"sayHello"];这一句引起的循环引用,导致控制器无法被释放,所以UIViewControllerdealloc才不会调用,从而引起内存泄露。解决的方式大致可以有两种:

  • MessageHandleremove移到其他方法中,例如viewWillDisappear

但是这种方式,不能满足所有的业务需求,如果你是想要在viewcontroller移除,也就是dealloc的时候,再移除对应的MessageHandle,而不希望在present或者push的情况下也移除对应的MessageHandle。那么只能先解决循环引用的问题,再进行MessageHandle的移除,而不是移除MessageHandle从而解决对应的循环引用,二者的因果关系是相反的!最终的目的都是为了解决内存泄露,方法如下:

  • 写一个新的controller,用新的controllerdelegate作为跳板绕回来,这是不是叫做协议转发呢?(知道的大神告知一声,感谢)
    具体如下图:
    OC和JS交互(二):WKWebView之MessageHandler_第4张图片
    解除循环引用.png

新的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, UIWebViewsWebViews具体的使用,之后另开一篇。

总结:

  • addScriptMessageHandler引起的循环引用,导致viewcontroller不能正常dealloc,写在dealloc中的removeScriptMessageHandlerForName也就不能正常调用,从而引起内存泄漏。具体的两种解决方式:
    1)在viewWillDisappear中调用removeScriptMessageHandlerForName,解除了循环引用并且能够正常remove
    2)通过一个中介性质的viewcontrollerdelegate作为跳板,解除循环引用,从而正常执行dealloc并正常remove

  • 关于OC和JS的传值,只能传一个参数,因此如果多个参数,可以通过字典或者json来作为参数。

你可能感兴趣的:(OC和JS交互(二):WKWebView之MessageHandler)