WKWebView详解
1. 概述
从iOS8开始,就引入了新的浏览器控件WKWebView,用于取代UIWebView,但是由于UIWebView的简单易用,还是使用率很高,目前苹果已经在迭代时,会发警告⚠️提醒更换控件,新应用必须使用WKWebView,到了告别UIWebView的时候了....
那么WKWebView究竟好在哪里呢?
- 内存开销更小
- 内置手势
- 支持更多H5特性
- 有Safari相同的JavaScript引擎
- 提供更多属性,比如加载进度、标题、准确的得到页面数等等
- 提供了更精细的加载流程回调(当然相比UIWebView看起来也更麻烦一些,毕竟方法多了)
1.1 UIWebView和WKWebView的流程对比
左边是UIWebView,右边是WKWebViewWKWebView的流程粒度更加细致,不但在请求的时候会询问WKWebView是否请求数据,还会在返回数据之后询问WKWebView是否加载数据
我曾经有个需求,点击链接的时候,如果是图片那就下载而不是跳转,用UIWebView就不好做,因为你不知道链接对应的到底是什么文件(有重定向),如果用WKWebView,我就可以在数据返回的时候判断MIMEType做出不同的跳转策略
2. WKWebView的基本使用
2.1 引入WKWebView
- 头文件引入
#import <WebKit/WebKit.h>
-
在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调原生
- 在UIWebView中,H5触发原生的函数,我们普遍做法约定好需要触发事件的链接规则,如果是普通超链接就放行,如果是特殊链接,就拦截下来,然后根据约定好的规则,拼凑出要调用的方法名称、参数等等信息
- 在WKWebView中,有一套解决js调用原生方法的规则
步骤:
-
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>
- 在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];
- 响应对象实现相关协议
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