Smallfan

逝岁流年,流年无恙

有趣的技术,有趣的世界


WKWebView

本文系 Smallfan 原创内容,转载请在文章开头显眼处注明作者和出处。

分析

在iPhone 6s、iOS 10.3.2中,对 http://www.qq.com 进行10次请求,得到如下数据:

次数 UIWebView 内存消耗 WKWebView 内存(APP)消耗 UIWebView 请求耗时 WKWebView 请求耗时
1 67.47 MB 0.81 MB 4.13 s 0.80 s
2 58.23 MB 0.86 MB 1.16 s 0.54 s
3 57.83 MB 0.50 MB 1.14 s 0.56 s
4 59.38 MB 0.88 MB 1.08 s 1.07 s
5 59.70 MB 0.75 MB 1.07 s 0.71 s
6 64.05 MB 0.83 MB 1.47 s 0.65 s
7 59.45 MB 0.81 MB 1.11 s 0.63 s
8 57.55 MB 0.45 MB 1.15 s 0.64 s
9 58.47 MB 0.77 MB 1.17s 0.75 s
10 58.89 MB 0.84 MB 1.11 s 0.70 s

UIWebView平均内存消耗:54.13 MB WKWebView平均(APP)内存消耗:0.75 MB UIWebView平均请求耗时:1.46 s WKWebView平均请求耗时:0.7 s

综上可得:WKWebView在请求耗时上为UIWebView的50%左右,内存上更是完胜。但实际上 WKWebView是一个多进程组件,网络请求以及UI渲染在其它进程中执行。仔细观察会发现:加载时,App进程内存消耗虽非常小甚至反而大幅下降,但Other Process的内存占用会增加。所以:在UIWebView上当内存占用太大的时候,App Process会crash;而在WKWebView上当总体的内存占用比较大的时候,WebContent Process会crash,从而出现白屏现象。

Tip: 在一些用webGL渲染的复杂页面,使用WKWebView总体的内存占用(App Process Memory + Other Process Memory)不见得比UIWebView少很多。

考虑全部替换WKWebView风险过高,可通过Server端在APP启动时下发URL列表的方式实现WKWebView的灰度能力。通过封装继承 UIViewSFWebView ,实现UIWebView与WKWebView双核能力WebView。

特性

关于WKWebView特性:

  • 在性能、稳定性、功能方面有很大提升;
  • 允许JavaScript的Nitro库加载并使用(UIWebView中限制);
  • 支持了更多的HTML5特性;
  • 高达60fps的滚动刷新率以及内置手势;
  • 将UIWebView 和 UIWebViewDelegate 重构成了14类与3个协议;

一些问题及解决方案

1.白屏问题

在UIWebView上当内存占用太大的时候,App Process会crash;而在WKWebView上当总体的内存占用比较大的时候,WebContent Process会crash,从而出现白屏现象。

实验链接: http://people.mozilla.org/~rnewman/fennec/mem.html

这个时候WKWebView.URL会变为nil, 简单的 reload 刷新操作已经失效,对于一些长驻的H5页面影响比较大。 解决方案:

  • 借助 WKNavigtionDelegate(仅适用iOS9以上)
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0));

当WKWebView总体内存占用过大,页面即将白屏的时候,系统会调用上面的回调函数,我们在该函数里执行 [webView reload] (这个时候webView.URL取值尚不为nil)解决白屏问题。在一些高内存消耗的页面可能会频繁刷新当前页面,H5侧也要做相应的适配操作。

  • 检测 webView.title 是否为空(需在初始化时设置默认webView.title)

并不是所有H5页面白屏的时候都会调用上面的回调函数,比如在一个高内存消耗的H5页面上present 系统相机,拍照完毕后返回原来页面的时候出现白屏现象(拍照过程消耗了大量内存,导致内存紧张,WebContent Process 被系统挂起),但上面的回调函数并没有被调用。在WKWebView白屏的时候,另一种现象是webView.title会被置空, 因此,可以在viewWillAppear的时候检测webView.title是否为空来 reload 页面。

综合以上两种方法可以解决绝大多数的白屏问题。

2.Cookie问题

2.1 Cookie的私有存储问题

业界普遍认为WKWebView拥有自己的私有存储,不会将cookie存入到标准的cookie容器NSHTTPCookieStorage中。 实践发现:在iOS 8上,当页面跳转的时候,当前页面的cookie会写入 NSHTTPCookieStorage 中,而在iOS 10上,JS执行 document.cookie 或服务器 set-cookie 注入的cookie会很快同步到 NSHTTPCookieStorage 中.

FireFox工程师曾建议通过 reset WKProcessPool来触发cookie同步到 NSHTTPCookieStorage 中,实践发现不起作用,并可能会引发当前页面 session cookie 丢失等问题。

2.2 请求不会自动带上容器中Cookie问题

WKWebView发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的cookie。 比如,NSHTTPCookieStorage 中存储了一个cookie:

name=Nicholas;value=test;domain=www.smallfan.net;expires=Sat, 02 May 2019 23:38:25 GMT;

通过UIWebView发起请求http://www.smallfan.net,则请求头会自动带上cookie: Nicholas=test; 而通过WKWebView发起请求http://www.smallfan.net,请求头不会自动带上cookie: Nicholas=test。 解决方案:

  • A. WKWebView loadRequest 前,在request的header中设置cookie, 解决首个请求cookie带不上的问题。
WKWebView *webView = [WKWebView new];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://www.fxiaoke.com"]];
[request addValue:@"uid=1000" forHTTPHeaderField:@"Cookie"];
[webView loadRequest:request];
  • B.通过·document.cookie·设置cookie解决后续页面(同域)Ajax、iframe请求的cookie问题。
- (NSString *)shareHttpCookieFromStorage:(NSURL *)url {

	NSMutableArray *array = [NSMutableArray array];
	for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:url]) {

		NSString *value = [NSString stringWithFormat:@"%@=%@", cookie.name, cookie.value];
		value = [NSString stringWithFormat:@"document.cookie = '%@'", value];
		[array addObject:value];
	}

	NSString *header = @"";
	header = [array componentsJoinedByString:@";"];

	return header;
}

- (void)addCookiesWithUrl:(NSURL *)url {
	WKUserContentController *userContentController = [WKUserContentController new]; 
	WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:[self shareHttpCookieFromStorage]  injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                                                         forMainFrameOnly:NO];
	[controller addUserScript:cookieScript];
	[userContentController addUserScript:cookieScript];
	_wkWebView = [[WKWebView alloc] initWithFrame:self.bounds configuration:configuration];
	...
}

注意:因为NSHTTPCookieStorage是整个APP共享的单例,包含了所有domain的cookie,在WKWebView初始化时,务必提前获取URL载入对应的cookie,防止因cookies泄漏造成可模仿登录等安全漏洞。

B方案无法解决302请求(跨域)的cookie问题,可以拦截页面每次跳转都会调用的回调函数: - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler来实现复制request对象,在request header中带上cookie并重新 loadRequest

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {

	NSMutableURLRequest *newReq = [navigationAction.request mutableCopy];
	NSMutableArray *array = [NSMutableArray array];
	for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:navagationAction.request.URL]) {
		NSString *value = [NSString stringWithFormat:@"%@=%@", cookie.name, cookie.value];
		[array addObject:value];
	}

	NSString *cookie = [array componentsJoinedByString:@";"];
	[newReq setValue:cookie forHTTPHeaderField:@"Cookie"];
	[webView loadRequest:newReq];
}

缺陷:依然解决不了页面iframe跨域请求的cookie问题,毕竟-[WKWebView loadRequest:]只适合加载mainFrame请求。

2.3 WKProcessPool无法本地化保存(离线缓存)

苹果开发者文档对WKProcessPool的定义是:A WKProcessPool object represents a pool of Web Content process. 通过让所有WKWebView共享同一个WKProcessPool实例,可以实现多个WKWebView之间共享cookie数据。不过WKProcessPool实例在app杀进程重启后会被重置,导致WKProcessPool中的cookie、session cookie数据丢失,目前也无法实现WKProcessPool实例本地化保存。 注意:由于WKWebView在请求过程中用户可能退出界面销毁对象,当请求回调时由于接收处理对象不存在,造成Bad Access crash,所以可将WKProcessPool设为单例 附使用方式:

static WKProcessPool *_sharedWKProcessPoolInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
	_sharedWKProcessPoolInstance = [[WKProcessPool alloc] init];
});

self.processPool = _sharedWKProcessPoolInstance;

WKWebViewConfiguration *configuration1 = [[WKWebViewConfiguration alloc] init];
configuration1.processPool = self.processPool;
WKWebView *webView1 = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration1];
...
WKWebViewConfiguration *configuration2 = [[WKWebViewConfiguration alloc] init];
configuration2.processPool = self.processPool;
WKWebView *webView2 = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration2];
...

3.NSURLProtocol问题

WKWebView在独立于 app 进程之外的进程中执行网络请求,请求数据不经过主进程,因此,在WKWebView上直接使用 NSURLProtocol 无法拦截请求。苹果开源的 Webkit2 源码暴露了私有API:

+ [WKBrowsingContextController registerSchemeForCustomProtocol:]

通过注册 http(s) scheme 后WKWebView将可以使用 NSURLProtocol 拦截http(s)请求:

//仅iOS8.4以上可用
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.4)  {
	Class cls = NSClassFromString(@"WKBrowsingContextController”); 
	SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");

	if ([(id)cls respondsToSelector:sel]) {
		#pragma clang diagnostic push
		#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

			// 注册http(s) scheme, 把 http和https请求交给 NSURLProtocol处理 
			[(id)cls performSelector:sel withObject:@"http"];
			[(id)cls performSelector:sel withObject:@"https"];

		#pragma clang diagnostic pop
		}
	}
}

这种方案目前存在以下严重缺陷: post请求body数据被清空 由于WKWebView在独立进程里执行网络请求。一旦注册http(s) scheme后,网络请求将从Network Process发送到App Process,这样 NSURLProtocol 才能拦截网络请求。在webkit2的设计里使用MessageQueue进行进程之间的通信,Network Process会将请求encode成一个Message,然后通过 IPC 发送给 App Process。出于性能的原因,encode的时候HTTPBody和HTTPBodyStream这两个字段被丢弃掉了。 因此,如果通过 registerSchemeForCustomProtocol 注册了http(s) scheme, 那么由WKWebView发起的所有http(s)请求都会通过 IPC 传给主进程 NSURLProtocol 处理,导致post请求body被清空。 解决方案:

3.1 如果没有开启ATS

可以注册customScheme, 比如smallfan://, 因此希望使用离线功能又不使用post方式的请求可以通过 customScheme 发起请求,比如 smallfan://webCache/HelloWorld ,然后在App进程 NSURLProtocol 拦截这个请求并加载离线数据。不足:使用post方式的请求该方案依然不适用,同时需要HTML5侧修改请求scheme以及CSP规则。

3.2 如果开启ATS

因为:一旦打开ATS开关:Allow Arbitrary Loads选项设置为NO,同时通过 registerSchemeForCustomProtocol 注册了http(s) scheme,WKWebView发起的所有非https网络请求将被阻塞(即便将Allow Arbitrary Loads in Web Content选项设置为YES)。 可通过hook所有的post请求的方式解决:

  • 对于Ajax post请求,思路是通过XMLHttpRequest send及open方法,将http body内容拼装在http header中并正常请求,App进程 NSURLProtocol 拦截这个请求,将header中的BODY内容取出置于body,发送请求,并将结果返回WKWebView(可借助 WebViewProxy 完成)。 JS文件:
var s_ajaxListener = new Object();
s_ajaxListener.tempOpen = XMLHttpRequest.prototype.open;
s_ajaxListener.tempSend = XMLHttpRequest.prototype.send;
s_ajaxListener.tempSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;

XMLHttpRequest.prototype.open = function(a,b) {
  this._method = a;
  s_ajaxListener.tempOpen.apply(this, arguments);
}

XMLHttpRequest.prototype.send = function(a,b) { 
  if (this._method && this._method.toLowerCase() == 'post') {
    a = encodeURIComponent(a)
    s_ajaxListener.tempSetRequestHeader.apply(this, ['BODY', a])
  }
  return s_ajaxListener.tempSend.apply(this, arguments);
}

APP拦截请求:

[WebViewProxy handleRequestsWithHttpHeader:@"BODY" handlerHash:[self hash] handler:^(NSURLRequest *req, WVPResponse *res) {

	NSURLSession *session = [NSURLSession sharedSession];
	NSMutableURLRequest *request = [req mutableCopy];
	request.HTTPMethod = @"POST";
	NSString *postData = request.allHTTPHeaderFields[@"BODY"];
	NSString *decodePostData = (__bridge_transfer NSString *)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL, (__bridge CFStringRef)postData, CFSTR(""), CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));
	NSString *httpBody = decodePostData;
	request.HTTPBody = [httpBody dataUsingEncoding:NSUTF8StringEncoding];
	NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
		NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
		[res respondWithNSData:data mimeType:response.MIMEType header:headers statusCode:((NSHTTPURLResponse *)response).statusCode];

	}];
	[dataTask resume];
}];
  • 对于form请求,解决方式类似Ajax,只是将BODY转编码后拼接到URL中,APP进程处理方式一致。

JS文件:

var s_formListener = window.onsubmit;
window.onsubmit = function (e) {
var node = e.srcElement;
if (node && node.tagName && node.tagName.toLowerCase() === 'form') {
	if (node.method && node.method.toLowerCase() === 'post') {
		var elements = [].slice.call(node.elements);
		var tempData = [], entryName, entryValue;
		for (var i = 0, l = elements.length; i < l; ++i) {
			entryName = elements[i].name;
			entryValue = elements[i].value;
			if (entryValue.toString() === '[object File]') {
				entryValue = entryValue.name;
			}
			tempData.push(encodeURIComponent(entryName) + '=' + encodeURIComponent(entryValue));
		}
		tempData = tempData.join('&');

		var action = node.action || location.href;
		var hashIndex = action.indexOf('#');
		if (hashIndex >= 0) {
			action = action.substring(0, hashIndex);
		}
		var queryIndex = action.indexOf('?');
		if (queryIndex >= 0) {
			action = action + '&POST_DATA=' + encodeURIComponent(tempData);
		} else {
			action = action + '?POST_DATA=' + encodeURIComponent(tempData);
		}

		node.action = action;
	}
}
if (s_formListener && s_formListener.apply) {
	s_formListener.apply(this, arguments);
}
}

APP拦截请求:

[WebViewProxy handleRequestsWithHttpHeader:@"POST_DATA" handler:^(NSURLRequest *req, WVPResponse *res) {

	NSURLSession *session = [NSURLSession sharedSession];
	NSMutableURLRequest *request = [req mutableCopy];
	request.HTTPMethod = @"POST";
	NSString *postData = request.allHTTPHeaderFields[@"POST_DATA"];
	NSString *decodePostData = (__bridge_transfer NSString *)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL, (__bridge CFStringRef)postData, CFSTR(""), CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));
	NSString *httpBody = decodePostData;
	request.HTTPBody = [httpBody dataUsingEncoding:NSUTF8StringEncoding];
	NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {

		NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
		[res respondWithNSData:data mimeType:response.MIMEType header:headers statusCode:((NSHTTPURLResponse *)response).statusCode];

	}];
	[dataTask resume];
}];

缺陷:Http header value及url字符有长度限制。在WWDC 2017上,提到iOS 11将开放一个 WKURLSchemeHandler 注册,提供custom response的能力,拭目以待。

4.JavaScript交互

4.1 WKWebView调用JavaScript

[_wkWebView evaluateJavaScript:@"Hello" completionHandler:^(NSString result, NSError * _Nullable error) {
	if([result isEqualToString:@"Hi"]) {
	}
}];

4.2 JavaScript调用WKWebView

 WKWebViewConfiguration * Configuration = [[WKWebViewConfiguration alloc] init];
 WKUserContentController *userContentController = [[WKUserContentController alloc] init];

//注册一个name为HelloNative的js方法
[userContentController addScriptMessageHandler:self  name:@"HelloNative"];

Configuration.userContentController = userContentController;
_wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, 300,500) configuration:Configuration];
#pragma mark WKScriptMessageHandler
//设置WKWebView的WKScriptMessageHandler代理方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message {
	if ([message.name isEqualToString:@"HelloNative"]) {
		// 打印所传过来的参数,只支持NSNumber, NSString, NSDate, NSArray, NSDictionary, NSNull类型
		NSLog(@"%@", message.body);
	}
}

JS调用:

window.webkit.messageHandlers.HelloNative.postMessage(message);

注意:关闭web页时,需要调用removeScriptMessageHandlerForName以防止内存泄漏。

- (void)dealloc {
	_wkWebView.UIDelegate = nil;
	_wkWebView.navigationDelegate = nil;
	[[_wkWebView configuration].userContentController removeScriptMessageHandlerForName:@"HelloNative"];
}

5.Crash问题

5.1 JS调用window.alert()函数引起的crash

当JS调用alert函数时,WKWebView使用如下方法回调:

+ (void)presentAlertOnController:(nonnull UIViewController*)parentController title:(nullable NSString*)title message:(nullable NSString *)message handler:(nonnull void (^)())completionHandler;

主要原因是上述 completionHandler 没有被调用导致的。在适配WKWebView的时候,我们需要自己实现该回调函数,window.alert()才能调起alert框。 解决方案:

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
	if (/*UIViewController of WKWebView has finish push or present animation*/) { 
		completionHandler();
		return;
	} 
	UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
	[alertController addAction:[UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]];
	if (/*UIViewController of WKWebView is visible*/)
		[self presentViewController:alertController animated:YES completion:^{}];
	else
		completionHandler();
}

5.2 WKWebView 退出前调用evaluateJavaScript:completionHandler:引起的crash

主要原因WKWebView退出并被释放后导致 completionHandler 变成野指针,而此时 JavaScriptCore还在执行JS代码,待JavaScriptCore执行完毕后会调用 completionHandler() ,导致crash。 这个crash只发生在iOS 8系统上,iOS9以上主要是对completionHandler block做了copy。

解决方案: 通过在completionHandlerretainWKWebView防止completionHandler被过早释放。

+ (void) load {
	[self jr_swizzleMethod:NSSelectorFromString(@"evaluateJavaScript:completionHandler:") withMethod:@selector(altEvaluateJavaScript:completionHandler:) error:nil];
}
/*
 * fix: WKWebView crashes on deallocation if it has pending JavaScript evaluation 
 */
- (void)altEvaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *))completionHandler {
	id strongSelf = self;
	[self altEvaluateJavaScript:javaScriptString completionHandler:^(id r, NSError *e)  {
		[strongSelf title];
		if (completionHandler) {
			completionHandler(r, e);
		}
	}];
}

6.进度条问题

在UIWebView中,进度条一直是个缺陷问题,尽管有 NJKWebViewProgress 一类开源组件,但在精确处理加载完成上还有些许不足(部分页面可见webViewDidStartLoad:与webViewDidFinishLoad:不成对回调,导致进度条加载结果计算有误)。而WKWebView上增加了一个estimatedProgress属性,通过KVO可实现精准进度条控制。iOS WKWebView添加类似微信的进度条 通过对微信的观察,进度条的精确结果并不是首要的,良好的用户心态预期才是其重点。所以,可以考虑以如下方式实现“虚拟”的进度条。

- (void)startProgress {
	if (_hideProgress) {
		return;
	}

	if (_progress == 0) {
		_progress = 0.9;

		[_progressLayer removeAllAnimations];//清除所有的动画

		CGRect frame = _progressView.frame;
		CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
		//设置关键帧数组
		animation.values=@[[NSValue valueWithCGPoint:CGPointMake(0, 0)],
						   [NSValue valueWithCGPoint:CGPointMake(0.7 * frame.size.width, .0)],
						   [NSValue valueWithCGPoint:CGPointMake(0.9 * frame.size.width, .0)],
						   [NSValue valueWithCGPoint:CGPointMake(1.0 * frame.size.width, .0)]];
		//设置每个关键帧对应的时间点,取值为0~1
		animation.keyTimes = @[[NSNumber numberWithFloat:.0],
							   [NSNumber numberWithFloat:.3],
							   [NSNumber numberWithFloat:.7],
							   [NSNumber numberWithFloat:1.]];
		animation.removedOnCompletion = YES;
		animation.fillMode = kCAFillModeForwards;
		animation.duration = 20;
		animation.delegate = self;
		[_progressLayer addAnimation:animation forKey:@"startProgress"];
	}
}

- (void)completeProgress {
	if (_hideProgress) {
		return;
	}

	CGPoint point = _progressLayer.presentationLayer.position;//当前动画的position
	if (round(point.x) == 0 && !_progress) {
		return;
	}
	[_progressLayer removeAnimationForKey:@"startProgress"];

	_progress = 1.0;

	CGRect frame = _progressView.frame;
	CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
	//设置关键帧数组
	animation.values=@[[NSValue valueWithCGPoint:point],
					   [NSValue valueWithCGPoint:CGPointMake(1.0 * frame.size.width, .0)]];
	//设置每个关键帧对应的时间点
	animation.keyTimes = @[[NSNumber numberWithFloat:.0],
						   [NSNumber numberWithFloat:1.]];
	animation.duration = .27;
	animation.removedOnCompletion = YES;
	animation.fillMode = kCAFillModeForwards;
	[_progressLayer addAnimation:animation forKey:@"completeProgress"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
	// 当WKWebView回调webView:didFinishNavigation:时,页面实际上渲染并未完成
	// 监听loading属性变化,可精确判断请求完成+渲染完成
	if ([keyPath isEqualToString:@"loading"]) {

		BOOL oldLoading = [[change objectForKey:NSKeyValueChangeOldKey] boolValue];
		BOOL newLoading = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];

		if (newLoading) {
			dispatch_async(dispatch_get_main_queue(), ^{
				[self startProgress];
			});
		} else if (!newLoading) {
			dispatch_async(dispatch_get_main_queue(), ^{
				[self completeProgress];
				_progress = 0;
			});
		}
	} else {
		[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
	}
}

- (void)dealloc {
	_wkWebView.UIDelegate = nil;
	_wkWebView.navigationDelegate = nil;

	//记得在销毁时释放监听
	[_wkWebView removeObserver:self forKeyPath:@"loading"];
}

7.截屏问题

- (UIImage*)imageSnapshot {
	UIGraphicsBeginImageContextWithOptions(self.bounds.size,YES,self.contentScaleFactor);
	[self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];
	UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
	UIGraphicsEndImageContext();
	return newImage;
}

使用以上方法对webGL页面的截屏结果不是空白就是纯黑图片。

解决方案:约定一个JS接口,让H5实现该接口,具体是通过 canvas getImageData() 方法取得图片数据后返回 base64 格式的数据,客户端在需要截图的时候,调用这个JS接口获取 base64 String 并转换成 UIImage

8.其他问题

8.1 iOS8.2以下音频无法停止

解决方案多种:

//第一种:返回时请求一个空页面
 [NSURL URLWithString:@"about:blank"]

//第二种:返回时加载一个空的HTML String
[_wkWebView loadHTMLString:@"<html/>" baseURL:nil]

//第三种:在viewDidDisappear方法里播放无声的音频再暂停

8.2 视频没有自动播放

解决方案: WKWebView需要通过 WKWebViewConfiguration.mediaPlaybackRequiresUserAction 设置是否允许自动播放,但一定要在WKWebView初始化之前设置,在WKWebView初始化之后设置无效。

8.3 页面回退问题

  • 业务上的需求,当最后只有一条历史,直接 pop 回去,需要如下改写。
- (BOOL)canGoBack {
	if (self.backForwardList.backList.count <= 1) {
		return NO;
	}
	return YES;
}
  • WKWebView上调用 -[WKWebView goBack] , 回退到上一个页面后不会触发 window.onload() 函数、不会执行JS。

8.4 页面回退时,字体变大

解决方案: 在页面 webView:didFinishNavigation: 中执行以下JavaScript将webkit中字体还原到100%

[self evaluateJavaScript:@"document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust= '100%'" completionHandler:nil];
参考文献:

1. 腾讯Bugly团队【WKWebView 那些坑】

最近的文章

基于 LocalWebServer 实现 WKWebView 离线资源加载

本文系 Smallfan 原创内容,转载请在文章开头显眼处注明作者和出处。背景笔者在《WKWebView》一文中提到过,WKWebView 在独立于 app 进程之外的进程中执行网络请求,请求数据不经过主进程,因此,在 WKWebView 上直接使用 NSURLProtocol 无法拦截请求。所以如果需要使用到拦截请求,有种可行地方案是使用苹果开源的 Webkit2 源码暴露的私有API(详见原文第3小节:NSURLProtocol问题)。但使用私有API,必然带来以下几个问题: 审...…

iOS-Webview继续阅读