随着苹果系统的不断升级,如今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协议,结束时需要移除
[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说再见了。