是时候放弃UIWebView

2017-12-08

随着苹果系统的不断升级,如今iOS11发布已经有段时间了,虽然此版本被吐槽为BUG版。但不可否认苹果爸爸还是积极向上,勇于创新的,系统还是在快速开发更新中的。此时我觉得是放弃UIWebView最佳时刻,首先,根据苹果官方统计,目前iOS设备,90%以上都已经升级到iOS9,因此对于远古时代的iOS7可以果断放弃。适配iOS8及以上即可。其次,虽然UIWebView伴随我们很久,但其表现却不尽如人意,内存性能暂且不说,最近公司前端同学使用新框架Vue.js开发之后,各种BUG层出不穷,如:网页title获取不到,web控件位置错乱,无法满足h5新增特性,等等。综合以上两方面,我觉得是时候放弃UIWebView,来重用WKWebView了!

WKWebView

iOS8之后苹果推荐使用WKWebView替代UIWebView,其主要特点:
1.在性能、稳定性、内存占用上有很大的提升。

2.将UIWebViewDelegate与UIWebView拆分成了14类与3个协议(更加强大,专业)

3.支持更多的HTML5特性

4.高达60fps的滚动刷新率以及内置手势;

5.可以通过KVO监控网络加载的进度,获取网页title;

虽然很多人反应WKWebView存在Cookie以及post参数等坑的存在(目前我还未发现),但瑕不掩瑜,更何况坑是可以填的,苹果也会不断升级更新的。

WKWebView于UIWebView使用上的区别

此处我主要讲述WKWebView和UIWebView使用上的不同之处,不在过多的讲述简单使用。

使用时先要导入:WebKit/WebKit.h
WKWebView主要新增以下几种协议:

1.WKNavigationDelegate:类似于UIWebView的加载成功、失败、是否允许跳转等

2.WKUIDelegate:主要是一些alert、打开新窗口之类的(不实现会有问题哦)

3.WKScriptMessageHandler:JS交互
首先,与UIWebView创建不同,每个WKWebView都需要一个WKWebViewConfiguration配置对象代码如下:

1
2
3
4
5
6
7
8
9
10
// 创建配置
WKWebViewConfiguration *cofiguration = [[WKWebViewConfiguration alloc] init];
// 创建UserContentController(提供JavaScript向webView发送消息的方法)
cofiguration.userContentController = [[WKUserContentController alloc] init];
// 添加消息处理,注意:self指代的对象需要遵守WKScriptMessageHandler协议,结束时需要移除
#pragma mark - JS调用OC
[cofiguration.userContentController addScriptMessageHandler:self name:@"NativeMethod"];
// 将UserConttentController设置到配置文件
// 高端的自定义配置创建WKWebView
WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds configuration:cofiguration];

可以看到WKWebView自己提供了和JS交互的方法,不再像UIWebView那样要通过拦截url或者导入JC框架来实现了。(WKWebView仍可以使用拦截url来实现,但不建议使用)
我们提供了方法给JS调用,方法回调在WKScriptMessageHandler完成:

1
2
3
4
5
6
7
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
// 判断是否是调用原生的
if ([@"NativeMethod" isEqualToString:message.name]) {
NSDictionary *jsReturn = message.body;
NSLog(@"JS调用了 方法 :%@",message.name);
NSLog(@"JS返回值: %@", jsReturn[@"body"])
}

注意:上面将当前ViewController设置为MessageHandler之后需要在当前ViewController销毁前将其移除,否则会造成内存泄漏。

1
2
3
- (void)dealloc {
[webView.configuration.userContentController removeScriptMessageHandlerForName:@"NativeMethod"];
}

有的同学可能发现Controller根本就没调用dealloc。后检查了ViewController中所有使用到self的地方,发现WKUserContentController的下面这个方法有使用到self

1
[cofiguration.userContentController addScriptMessageHandler:self name:@"NativeMethod"];

可能造成循环引用无法释放。解决方法:自己创建一个WeakScriptMessageDelegate,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//.h
@interface OCTWeakScriptMessageDelegate :NSObject<WKScriptMessageHandler>
@property (nonatomic, weak) id<WKScriptMessageHandler> scriptDelegate;
- (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate;
@end

//.m
@implementation OCTWeakScriptMessageDelegate
- (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate {
self = [super init];
if (self) {
_scriptDelegate = scriptDelegate;
}
return self;
}
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
[self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message];
}
@end

新的调用:

1
2
[cofiguration.userContentController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"NativeMethod"];

之后dealloc就会正常调用了。

OC调用JS可以直接使用下面方法调用,需要注意的一点必须放在加载完成之后调用。

1
2
3
4
5
6
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation {
//获取title
[webView evaluateJavaScript:@"document.title" completionHandler:^(id _Nullable title, NSError * _Nullable error) {
NSLog(@"调用evaluateJavaScript异步获取title:%@", title);
}];
}

WKWebView可以通过KVO来坚听进度条和title别忘了也要移除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//监听开始加载
[webView addObserver:self forKeyPath:@"loading"
options:NSKeyValueObservingOptionNew context:nil];
//监听开始加载进度
[webView addObserver:self
forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew
context:nil];
//监听开始网页title
[webView addObserver:self forKeyPath:@"title"
options:NSKeyValueObservingOptionNew context:nil];
//KVO实现
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"loading"])
{
NSLog(@"loading");
} else if ([keyPath isEqualToString:@"title"])
{
self.title = self.webView.title;
} else if ([keyPath isEqualToString:@"estimatedProgress"])
{
NSLog(@"progress: %f", self.webView.estimatedProgress);
}
// 加载完成
if (!self.webView.loading)
{
NSLog(@"progress加载完成: %f", self.webView.estimatedProgress);
}
}

移除KVO

1
2
3
4
5
6
- (void)dealloc {
NSLog(@"%@==> dealloc",[self class]);
[webView removeObserver:self forKeyPath:@"loading" context:nil];//移除kvo
[webView removeObserver:self forKeyPath:@"title" context:nil];
[webView removeObserver:self forKeyPath:@"estimatedProgress" context:nil];
}

对于WKUIDelegate需要我们自己重新实现否则js系统弹框会无效!固定代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(nonnull void (^)(void))completionHandler {
//js 里面的alert实现,如果不实现,网页的alert函数无效
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
}]];
[self presentViewController:alertController animated:YES completion:^{}];
}
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {
// js 里面的alert实现,如果不实现,网页的alert函数无效 ,
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action){
completionHandler(NO);
}]];
[alertController addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
completionHandler(YES);
}]];
[self presentViewController:alertController animated:YES completion:^{}];
}
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler
{
//用于和JS交互,弹出输入框
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:nil preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action){
completionHandler(nil);
}]];
[alertController addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
UITextField *textField = alertController.textFields.firstObject;
completionHandler(textField.text);
}]];
[alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
textField.text = defaultText;
}];
[self presentViewController:alertController animated:YES completion:NULL];
}

对于可能引起的白屏问题解决方法:

1
2
3
4
//WKWebView 白屏问题
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {
[webView reload];
}

还有User-Agent修改,我们可以仍然使用UIWebView修改全局的userAgent需要在app启动时调用,方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)configUserAgent{
UIWebView *webView = [[UIWebView alloc]initWithFrame:CGRectZero];
NSString* secretAgent = [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
NSMutableString * mutableSecretAgent = [NSMutableString stringWithString:secretAgent];
if ([mutableSecretAgent rangeOfString:@"拼接版本号"].location == NSNotFound) {
NSDictionary * info = [[NSBundle mainBundle] infoDictionary];
NSString *version =[NSString stringWithFormat:@"拼接版本号%@",info[@"CFBundleShortVersionString"]];
[mutableSecretAgent appendString:version];
NSDictionary *dictionary = [[NSDictionary alloc]
initWithObjectsAndKeys:mutableSecretAgent, @"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
}
webView = nil;
}

此方法的不足就是这样app内所有的webview都被修改。
WKWebView可以使用下面方法:

1
2
3
4
5
6
7
8
9
// 获取默认User-Agent(iOS9 之后可用 self.webView.customUserAgent)
[self.webView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id result, NSError *error) {
NSString *oldAgent = result;
// 给User-Agent添加额外的信息
NSString *newAgent = [NSString stringWithFormat:@"%@;%@", oldAgent, @"extra_user_agent"];
// 设置global User-Agent
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:newAgent, @"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
}];

h5页面内打开新网页无法响应问题

最近遇到点击h5某些跳转按钮无法跳转到相应网页,经调试发现点击按钮未走任何web代理,最后得知是h5跳出了本页面打开了新页面所至,解决办法实现如下代理:

1
2
3
4
5
6
7
-(WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
//如果是跳转一个新页面
if (!navigationAction.targetFrame.isMainFrame || navigationAction.targetFrame == nil) {
[webView loadRequest:navigationAction.request];
}
return nil;
}

总结:

在废弃UIWebView,使用WKWebView以后,避免了很多由HTML5新特性引起的bug,也提高了网页性能,提高了开发效率,总之在iOS11之后,是时候和UIWebView说再见了。