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