HOME> 克罗斯世界杯> WKWebView详解 2025-09-25 02:57:33
WKWebView详解

1. 概述

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

那么WKWebView究竟好在哪里呢?

内存开销更小

内置手势

支持更多H5特性

有Safari相同的JavaScript引擎

提供更多属性,比如加载进度、标题、准确的得到页面数等等

提供了更精细的加载流程回调(当然相比UIWebView看起来也更麻烦一些,毕竟方法多了)

1.1 UIWebView和WKWebView的流程对比

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

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

左边是UIWebView,右边是WKWebView

2. WKWebView的基本使用

2.1 引入WKWebView

头文件引入#import

在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 *)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 *)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”

在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

Siri突然打开怎么回事
深入探讨微信聊天记录的保存时限与管理对策