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

参考

一、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方法是成对出现的,虐狗啊:

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 <UIKit/UIKit.h>

@interface WKWebViewController : UIViewController

@end

WKWebViewController.m

#import <WebKit/WebKit.h>
#import <Masonry.h>

@interface WKWebViewController ()<WKNavigationDelegate,WKUIDelegate,WKScriptMessageHandler>{
    
}
@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如下:

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
        <style type='text/css'>
            html { font-family:Helvetica; color:#222; }
            button { margin:0 3px 10px; font-size:16px; }
        </style>

    <script type="text/javascript">
        function say(){
            window.webkit.messageHandlers.sayHello.postMessage({body:'hello world'});
        }
    </script>
    <title>this is test page</title>
</head>
<body>
    <button onclick="say()"> say hello </button>
</body>
</html>

其中,关键的是say()方法中的这句写法,注册一个与OC同名注册的方法sayHello,传递的参数是一个字典:{body:'hello world'}

window.webkit.messageHandlers.sayHello.postMessage({body:'hello world'});

这一句的写法,可以在WKUserContentController类中找到:

postMessage.png

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

可能遇到的问题

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

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

循环引用.png

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

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

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

  • 写一个新的controller,用新的controllerdelegate作为跳板绕回来,这是不是叫做协议转发呢?(知道的大神告知一声,感谢)
    具体如下图:
    解除循环引用.png

新的controller就叫WXX_WKUserContentController具体的写法如下:

WXX_WKUserContentController.h

#import <UIKit/UIKit.h>
#import <WebKit/WebKit.h>

@protocol WXX_WKScriptMessageHandler <NSObject>
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
@end

@interface WXX_WKUserContentController : UIViewController<WKScriptMessageHandler>

@property(weak,nonatomic)id<WXX_WKScriptMessageHandler>delegate;

@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 <WebKit/WebKit.h>
#import <Masonry.h>
#import "WXX_WKUserContentController.h"

@interface WKWebViewController ()<WKNavigationDelegate,WKUIDelegate,WXX_WKScriptMessageHandler>{
    
}
@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:

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
        <style type='text/css'>
            html { font-family:Helvetica; color:#222; }
            button { margin:0 3px 10px; font-size:16px; }
        </style>

    <script type="text/javascript">
    
    function js_method(){
        return 'this is js method return value';
    }
    </script>

    <title>this is test page</title>
</head>
<body>
    
</body>
</html>

调用:

-(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来作为参数。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,193评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,306评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,130评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,110评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,118评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,085评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,007评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,844评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,283评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,508评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,395评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,985评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,630评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,797评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,653评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,553评论 2 352

推荐阅读更多精彩内容