WKWebView详解

1. 概述

从iOS8开始,就引入了新的浏览器控件WKWebView,用于取代UIWebView,但是由于UIWebView的简单易用,还是使用率很高,目前苹果已经在迭代时,会发警告⚠️提醒更换控件,新应用必须使用WKWebView,到了告别UIWebView的时候了....

那么WKWebView究竟好在哪里呢?

  1. 内存开销更小
  2. 内置手势
  3. 支持更多H5特性
  4. 有Safari相同的JavaScript引擎
  5. 提供更多属性,比如加载进度、标题、准确的得到页面数等等
  6. 提供了更精细的加载流程回调(当然相比UIWebView看起来也更麻烦一些,毕竟方法多了)

1.1 UIWebView和WKWebView的流程对比

WKWebView的流程粒度更加细致,不但在请求的时候会询问WKWebView是否请求数据,还会在返回数据之后询问WKWebView是否加载数据

我曾经有个需求,点击链接的时候,如果是图片那就下载而不是跳转,用UIWebView就不好做,因为你不知道链接对应的到底是什么文件(有重定向),如果用WKWebView,我就可以在数据返回的时候判断MIMEType做出不同的跳转策略

左边是UIWebView,右边是WKWebView

2. WKWebView的基本使用

2.1 引入WKWebView

  1. 头文件引入#import <WebKit/WebKit.h>
  2. 在targets中添加WebKit.framework库


    WebKit.framework

2.2 WKWebView初始化

可以在初始化的时候,加入一些配置选项

- (WKWebView *)webView
{
    if (nil == _webView) {
        // 可以做一些初始化配置定制
        WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
        configuration.selectionGranularity = WKSelectionGranularityDynamic;
        configuration.allowsInlineMediaPlayback = YES;
        
        WKPreferences *preferences = [WKPreferences new];
        //是否支持JavaScript
        preferences.javaScriptEnabled = YES;
        //不通过用户交互,是否可以打开窗口
        preferences.javaScriptCanOpenWindowsAutomatically = YES;
        configuration.preferences = preferences;
        
        // 初始化WKWebView
        _webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds configuration:configuration];

        // 有两种代理,UIDelegate负责界面弹窗,navigationDelegate负责加载、跳转等
        _webView.UIDelegate = self;
        _webView.navigationDelegate = self;
    }
    return _webView;
}

2.3 WKNavigationDelegate协议方法

#pragma mark - WKNavigationDelegate
/* 页面开始加载 */
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation{
}
/* 开始返回内容 */
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation{
     
}
/* 页面加载完成 */
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{
     
}
/* 页面加载失败 */
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation{
     
}
/* 在发送请求之前,决定是否跳转 */
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
    //允许跳转
    decisionHandler(WKNavigationActionPolicyAllow);
    //不允许跳转
    //decisionHandler(WKNavigationActionPolicyCancel);
}
/* 在收到响应后,决定是否跳转 */
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
     
    NSLog(@"%@",navigationResponse.response.URL.absoluteString);
    //允许跳转
    decisionHandler(WKNavigationResponsePolicyAllow);
    //不允许跳转
    //decisionHandler(WKNavigationResponsePolicyCancel);
}

2.4 UIDelegate协议的主要方法及应用

特别需要注意这个协议,与UIWebView不同,在WKWebView中,如果H5页面调用了window对象的alert,confirm,prompt方法,默认不会有任何反应,它内部会回调给你,必须由原生这边实现相关的弹窗

感觉这东西设计的很鸡肋,特别是初学者很喜欢alert一下看效果,结果一直点没反应,真是个大坑

#pragma mark - WKNavigationDelegate
#pragma mark - WKUIDelegate
/// 处理alert弹窗事件
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
    
    [self alert:@"温馨提示" message:message?:@"" buttonTitles:@[@"确认"] handler:^(int index, NSString *title) {
       completionHandler();
    }];
}

/// 处理Confirm弹窗事件
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{
    
    [self alert:@"温馨提示" message:message?:@"" buttonTitles:@[@"取消", @"确认"] handler:^(int index, NSString *title) {
       completionHandler(index != 0);
    }];
}

/// 处理TextInput弹窗事件
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler {
    
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"温馨提示" message:prompt preferredStyle:UIAlertControllerStyleAlert];
    [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        textField.text = defaultText;
    }];
    [alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(nil);
    }]];
    [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        NSString *text = [alert.textFields firstObject].text;
        NSLog(@"字符串:%@", text);
        completionHandler(text);
    }]];
    [self presentViewController:alert animated:YES completion:nil];
}

#pragma mark - 弹窗
- (void)alert:(NSString *)title message:(NSString *)message {
    [self alert:title message:message buttonTitles:@[@"确定"] handler:nil];
}

- (void)alert:(NSString *)title message:(NSString *)message buttonTitles:(NSArray *)buttonTitles handler:(void(^)(int, NSString *))handler {
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
    for (int i = 0; i < buttonTitles.count; i++) {
        [alert addAction:[UIAlertAction actionWithTitle:buttonTitles[i] style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
            if (handler) {
                handler(i, action.title);
            }
        }]];
    }
    [self presentViewController:alert animated:YES completion:nil];
}

3. WKWebView更多实战细节

3.1 动态更新标题

导航栏标题经常要根据当前H5页面标题更换,以前都是在页面加载完成后,使用window.document.title来获取,现在WKWebView提供了相关字段,我们只需要监听这个字段即可

[self.webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:NULL];
#pragma mark - 属性监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    if ([keyPath isEqualToString:@"title"]) {
        NSString *title = (NSString *)change[NSKeyValueChangeNewKey];
        self.title = title;
    }
}

3.2 动态加载进度条

以前UIWKWebView无法获取加载进度,只能知晓开始加载和结束加载,因此以前的做法是做一个假的进度条,等到结束的时候再突然设置成100%

WKWebView提供了estimatedProgress来监听加载进度,提供了loading来获取加载状态,我们可以拖个UIProgressView来显示进度(也很多人用layer来做,还可以做渐变的效果,视觉上更优)

@property (weak, nonatomic) IBOutlet UIProgressView *progressView;

[self.webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:NULL];
[self.webView addObserver:self forKeyPath:@"loading" options:NSKeyValueObservingOptionNew context:NULL];
#pragma mark - 属性监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    if ([keyPath isEqualToString:@"estimatedProgress"]) {
        CGFloat estimatedProgress = [change[NSKeyValueChangeNewKey] floatValue];
        NSLog(@"页面加载进度:%f", estimatedProgress);
        [self.progressView setProgress:estimatedProgress];
    }else if ([keyPath isEqualToString:@"loading"]) {
        BOOL loading = [change[NSKeyValueChangeNewKey] boolValue];
        NSLog(@"%@", loading ? @"开始加载" : @"停止加载");
        self.progressView.hidden = !loading;
    }
}

3.3 获取已打开页面数量

UIWebView提供了pagecount,但是没有卵用,不准确;WKWebView中有backForwardList记录了可回退的页面信息,已打开页面数量 = backForwardList数量 + 当前1页

int pageCount = self.webView.backForwardList.count + 1;

4. JS交互

4.1 原生调H5

简单易用,第一参数传执行的js方法,第二个block中回调执行后的结果,如果没有返回值,可以忽略这个block

[self.webView evaluateJavaScript:@"prompt('请输入您的名字:', '哈利波特')" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        
        if (error) {
            NSLog(@"error: %@", error);
        }else {
            NSLog(@"obj: %@", result);
        }
    }];

4.2 H5调原生

  1. 在UIWebView中,H5触发原生的函数,我们普遍做法约定好需要触发事件的链接规则,如果是普通超链接就放行,如果是特殊链接,就拦截下来,然后根据约定好的规则,拼凑出要调用的方法名称、参数等等信息
  2. 在WKWebView中,有一套解决js调用原生方法的规则

步骤:

  1. window.webkit.messageHandlers.<#对象名#>.postMessage(<#参数#>),这个对象名称只是个别名(不是非要对应我们哪个对象名称),跟前端协商好即可,比如我这里起名“target”
<script>
    $("#shoot").click(function () {
// 这里按照约定好的规则,触发的时候按照特定对象发送消息,传达到原生中
// 实际开发中,还要考虑多端交互的兼容性问题(iOS、Android、wechat)
        window.webkit.messageHandlers.target.postMessage({action: '开枪射击'});
    });

    $("#refull").click(function () {
        window.webkit.messageHandlers.target.postMessage({action: '上子弹'});
    });
</script>
  1. 在iOS端,添加js脚本的响应对象

注册告诉WKWebView都有哪些对象要来响应js事件,分别叫什么名字

// H5调用原生格式:window.webkit.messageHandlers.{name}.postMessage(参数);
// window.webkit.messageHandlers.target.postMessage({action: '开枪射击'});
// 在原生中注册好能响应js方法的类和注册别名,然后js按照以上方式调用,可以进入OC的didReceiveScriptMessage代理方法
[userContentController addScriptMessageHandler:self name:target];
  1. 响应对象实现相关协议

WKWebView会把触发回调给我们的协议方法,响应对象实现它即可

#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    
    NSString *name = [NSString stringWithFormat:@"执行对象名称:%@", message.name];
    NSString *params = [NSString stringWithFormat:@"附带参数:%@", [message.body description]];
}

4.2.1 H5调原生的内存泄露问题

问题:当执行addScriptMessageHandler方法时,如果传入的是当前控制器,控制器会被WKWebView强引用(就算你传入weak都没用,内部还是转成强引用),而当前控制器强引用着WKWebView,就成了循环引用

解决方式

方式一

在合适的时机添加和移除

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    // 注册响应H5调原生埋点
    WKUserContentController *userContentController = self.webView.configuration.userContentController;
    
    // H5调用原生格式:window.webkit.messageHandlers.{name}.postMessage(参数);
    // window.webkit.messageHandlers.target.postMessage({action: '开枪射击'});
    // 在原生中注册好能响应js方法的类和注册别名,然后js按照以上方式调用,可以进入OC的didReceiveScriptMessage代理方法
    [userContentController addScriptMessageHandler:self name:KCNAME];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    
    // 注册过的对象,移除,否则有内存泄露的问题
    [self.webView.configuration.userContentController removeScriptMessageHandlerForName:KCNAME];
}

方式二

其实苹果这么设计,应该是希望我们传入一个单独实现了WKScriptMessageHandler的对象,用来响应相关js交互操作,而不是传入当前控制器

参考文章:https://www.cnblogs.com/guohai-stronger/p/10234571.html

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