深入学习理解WebView
2017-03-03 » iOS开发基础UIWebView
WKWebView
一、WKWebView简介
UIWebView自iOS2就有,WKWebView从iOS8才有,毫无疑问WKWebView将逐步取代笨重的UIWebView。通过简单的测试即可发现UIWebView占用过多内存,且内存峰值更是夸张。WKWebView网页加载速度也有提升,但是并不像内存那样提升那么多。下面列举一些其它的优势:
1、更多的支持HTML5的特性
2、官方宣称的高达60fps的滚动刷新率以及内置手势
3、Safari相同的JavaScript引擎,且允许JavaScript的Nitro库加载并使用(UIWebView中限制);
4、将UIWebViewDelegate与UIWebView拆分成了14类与3个协议(官方文档说明)
5、占用更少的内存,在性能、稳定性、功能方面有很大提升(最直观的体现就是加载网页是占用的内存,模拟器加载百度与开源中国网站时,WKWebView占用23M,而UIWebView占用85M);
另外用的比较多的,增加加载进度属性:estimatedProgress
二、WKWebView初始化
1. 首先需要引入WebKit库
import
2. 初始化方法分为以下两种
` ``// 默认初始化`` ``- (instancetype)initWithFrame:(CGRect)frame;`` ``// 根据对webview的相关配置,进行初始化`` ``- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;`
3. 加载网页与HTML代码的方式与UIWebView相同,代码如下:
` ``WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds];`` ``[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@``"http://www.baidu.com"``]]];`` ``[self.view addSubview:webView];`
三、属性介绍
`WKBackForwardList:之前访问过的 web页面的列表,可以通过后退和前进动作来访问到。``WKBackForwardListItem: webview中后退列表里的某一个网页。``WKFrameInfo: 包含一个网页的布局信息。``WKNavigation: 包含一个网页的加载进度信息。``WKNavigationAction:包含可能让网页导航变化的信息,用于判断是否做出导航变化。``WKNavigationResponse:包含可能让网页导航变化的返回内容信息,用于判断是否做出导航变化。``WKPreferences: 概括一个 webview 的偏好设置。``WKProcessPool: 表示一个 web 内容加载池。``WKUserContentController: 提供使用 JavaScript post 信息和注射 script 的方法。``WKScriptMessage: 包含网页发出的信息。``WKUserScript:表示可以被网页接受的用户脚本。``WKWebViewConfiguration: 初始化 webview 的设置。``WKWindowFeatures: 指定加载新网页时的窗口属性。``WKNavigationDelegate: 提供了追踪主窗口网页加载过程和判断主窗口和子窗口是否进行页面加载新页面的相关方法。``WKScriptMessageHandler: 提供从网页中收消息的回调方法。``WKUIDelegate: 提供用原生控件显示网页的方法回调。`
四、WKWebView代理
WKWebView有两个delegate,WKUIDelegate 和 WKNavigationDelegate。WKNavigationDelegate主要处理一些跳转、加载处理操作,WKUIDelegate主要处理JS脚本,确认框,警告框等。因此WKNavigationDelegate更加常用。
1. 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 didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation{`` ` `}``// 在收到响应后,决定是否跳转``- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{`` ` ` ``NSLog(@``"%@"``,navigationResponse.response.URL.absoluteString);`` ``//允许跳转`` ``decisionHandler(WKNavigationResponsePolicyAllow);`` ``//不允许跳转`` ``//decisionHandler(WKNavigationResponsePolicyCancel);``}``// 在发送请求之前,决定是否跳转``- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{`` ` ` ``NSLog(@``"%@"``,navigationAction.request.URL.absoluteString);`` ``//允许跳转`` ``decisionHandler(WKNavigationActionPolicyAllow);`` ``//不允许跳转`` ``//decisionHandler(WKNavigationActionPolicyCancel);``}`
2. WKUIDelegate
`#pragma mark - WKUIDelegate``// 创建一个新的WebView``- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures{`` ``return` `[[WKWebView alloc]init];``}``// 输入框``- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * __nullable result))completionHandler{`` ``completionHandler(@``"http"``);``}``// 确认框``- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler{`` ``completionHandler(YES);``}``// 警告框``- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{`` ``NSLog(@``"%@"``,message);`` ``completionHandler();``}`
3. WKScriptMessageHandler
这个协议中包含一个必须实现的方法,这个方法是提高App与web端交互的关键,它可以直接将接收到的JS脚本转为OC或Swift对象。(当然,在UIWebView也可以通过“曲线救国”的方式与web进行交互,著名的Cordova框架就是这种机制)
`// 从web界面中接收到一个脚本时调用``- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;`
4、WKWebView加载JS
`// 图片缩放的js代码``NSString *js = @``"var count = document.images.length;for (var i = 0; i < count; i++) {var image = document.images[i];image.style.width=320;};window.alert('找到' + count + '张图');"``;``// 根据JS字符串初始化WKUserScript对象``WKUserScript *script = [[WKUserScript alloc] initWithSource:js injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];``// 根据生成的WKUserScript对象,初始化WKWebViewConfiguration``WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];``[config.userContentController addUserScript:script];``_webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];``[_webView loadHTMLString:@``""``baseURL:nil];``[self.view addSubview:_webView];`
五、WKWebView使用过程中的坑
1、WKWebView下面添加自定义View
因为我们有个需求是在网页下面在添加一个View,用来展示此链接内容的相关评论。在使用UIWebView的时候,做法非常简单粗暴,在UIWebView的ScrollView后面添加一个自定义View,然后根据View的高度,在改变一下scrollView的contentSize属性。以为WKWebView也可以这样简单粗暴的去搞一下,结果却并不是这样。
首先改变WKWebView的scrollView的contentSize属性,系统会在下一次帧率刷新的时候,再给你改变回原有的,这样这条路就行不通了。我马上想到了另一个办法,改变scrollView的contentInset这个系统倒不会在变化回原来的,自以为完事大吉。后来过了两天,发现有些页面的部分区域的点击事件无法响应,百思不得其解,最后想到可能是设置的contentInset对其有了影响,事实上正是如此。查来查去,最后找到了一个解决办法是,就是当页面加载完成时,在网页下面拼一个空白的div,高度就是你添加的View的高度,让网页多出一个空白区域,自定义的View就添加在这个空白的区域上面。这样就完美解决了此问题。具体可参考Demo所写,核心代码如下:
`self.addView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, ScreenWidth, addViewHeight)];``self.addView.backgroundColor = [UIColor redColor];``[self.webView.scrollView addSubview:self.addView];``NSString *js = [NSString stringWithFormat:@"\`` ``var` `appendDiv = document.getElementById(\"AppAppendDIV\");\`` ``if` `(appendDiv) {\`` ``appendDiv.style.height = %@+\"px\";\`` ``} ``else` `{\`` ``var` `appendDiv = document.createElement(\"div\");\`` ``appendDiv.setAttribute(\"id\",\"AppAppendDIV\");\`` ``appendDiv.style.width=%@+\"px\";\`` ``appendDiv.style.height=%@+\"px\";\`` ``document.body.appendChild(appendDiv);\`` ``}\`` ``", @(addViewHeight), @(self.webView.scrollView.contentSize.width), @(addViewHeight)];`` ` ` ``[self.webView evaluateJavaScript:js completionHandler:nil];`
2、WKWebView加载HTTPS的链接
HTTPS已经越来越被重视,前面我也写过一系列的HTTPS的相关文章HTTPS从原理到应用(四):iOS中HTTPS实际使用当加载一些HTTPS的页面的时候,如果此网站使用的根证书已经内置到了手机中这些HTTPS的链接可以正常的通过验证并正常加载。但是如果使用的证书(一般为自建证书)的根证书并没有内置到手机中,这时是链接是无法正常加载的,必须要做一个权限认证。开始在UIWebView的时候,是把请求存储下来然后使用NSURLConnection去重新发起请求,然后走NSURLConnection的权限认证通道,认证通过后,在使用UIWebView去加载这个请求。
在WKWebView中,WKNavigationDelegate中提供了一个权限认证的代理方法,这是权限认证更为便捷。代理方法如下:
`- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {`` ``if` `([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {`` ` ` ``if` `([challenge previousFailureCount] == 0) {`` ` ` ``NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];`` ` ` ``completionHandler(NSURLSessionAuthChallengeUseCredential, credential);`` ` ` ``} ``else` `{`` ` ` ``completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);`` ` ` ``}`` ` ` ``} ``else` `{`` ` ` ``completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); `` ` ` ``}``}`
这个方法比原来UIWebView的认证简单的多。但是使用中却发现了一个很蛋疼的问题,iOS8系统下,自建证书的HTTPS链接,不调用此代理方法。查来查去,原来是一个bug,在iOS9中已经修复,这明显就是不管iOS8的情况了,而且此方法也没有标记在iOS9中使用,这点让我感到有点失望。这样我就又想到了换回原来UIWebView的权限认证方式,但是试来试去,发现也不能使用了。所以关于自建证书的HTTPS链接在iOS8下面使用WKWebView加载,我没有找到很好的办法去解决此问题。这样我不得已有些链接换回了HTTP,或者在iOS8下面在换回UIWebView。如果你有解决办法,也欢迎私信我,感激不尽。
3、WKWebView和JavaScript交互
WKWebView和JavaScript交互,在WKUserContentController.h这个头文件中- (void)addScriptMessageHandler:(id )scriptMessageHandler name:(NSString *)name;这个方法的注释中已经明确给出了交互办法。使用起来倒是非常的简单。创建WKWebView的时候添加交互对象,并让交互对象实现WKScriptMessageHandler中的唯一的一个代理方法。具体的方式参考Demo中的使用。
`// 添加交互对象``[config.userContentController addScriptMessageHandler:(id)self.ocjsHelper name:@``"timefor"``];``// 代理方法``- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;`
JavaScript调用Objective-C的时候,使用window.webkit.messageHandlers.timefor.postMessage({code: ‘0001’, functionName: ‘getdevideId’}); Objective-C自动对交互参数包装成了WKScriptMessage对象,其属性body则为传送过来的参数,name为添加交互对象的时候设置的名字,以此名字可以过滤掉不属于自己的交互方法。其中body可以为NSNumber, NSString, NSDate, NSArray, NSDictionary, and NSNull。
而Objective-C在回调JavaScript的时候,不能像我原来在 Objective-C与JavaScript交互的那些事这篇文章中写的那样,JavaScript传过来一个匿名函数,Objective-C这边直接调用一下就完事。WKWebView没有办法传过来一个匿名函数,所以回调方式,要么执行一段JavaScript代码,或者就是调用JavaScript那边的一个全局函数。一般是采用后者,至于Web端虽说暴露了一个全局函数,同样可以把这一点代码处理的很优雅。Objective-C传给JavaScript的参数,可以为Number, String, and Object。参考如下:
`// 数字``NSString *js = [NSString stringWithFormat:@``"globalCallback(%@)"``, number];``[self.webView evaluateJavaScript:js completionHandler:nil];``// 字符串``NSString *js = [NSString stringWithFormat:@``"globalCallback(\'%@\')"``, string];``[self.webView evaluateJavaScript:js completionHandler:nil];``// 对象``NSString *js = [NSString stringWithFormat:@``"globalCallback(%@)"``, @{@``"name"` `: @``"timefor"``}];``[self.webView evaluateJavaScript:js completionHandler:nil];``// 带返回值的JS函数``[self.webView evaluateJavaScript:@``"globalCallback()"` `completionHandler:^(id result, NSError * _Nullable error) { `` ``// 接受返回的参数,result中``}];`
参考资料
WKWebView默认禁止了一些跳转
- UIWebView 打开ituns.apple.com跳转到appStore, 拨打电话, 唤起邮箱等一系列操作UIWebView默认支持的.
- WKWebView 默认禁止了以上行为,除此之外,js端通过alert()`弹窗的动作也被禁掉了. 如何支持呢?
- 首先要设置WKWebView的
WKUIDelegate,并实现以下方法
WebView性能优化总结
一个加载网页的过程中,native、网络、后端处理、CPU都会参与,各自都有必要的工作和依赖关系;让他们相互并行处理而不是相互阻塞才可以让网页加载更快:
- WebView初始化慢,可以在初始化同时先请求数据,让后端和网络不要闲着。
- 后端处理慢,可以让服务器分trunk输出,在后端计算的同时前端也加载网络静态资源。
- 脚本执行慢,就让脚本在最后运行,不阻塞页面解析。
- 同时,合理的预加载、预缓存可以让加载速度的瓶颈更小。
- WebView初始化慢,就随时初始化好一个WebView待用。
- DNS和链接慢,想办法复用客户端使用的域名和链接。
- 脚本执行慢,可以把框架代码拆分出来,在请求页面之前就执行好。