白屏问题
UIWebView上当内存占用过大时,App会crash; WKWebView上当内存占用过大时,WebContent process会crash,导致白屏。 此时,wkWebView的url变为nil,reload操作已无效。
所以,白屏的根本原因是由内存占用过大引发的App crash问题,转换成了WebContent process的crash问题。
解决方法
WKNavigationDelegate的回调方法webViewWebContentProcessDidTerminate
在webViewWebContentProcessDidTerminate回调中执行webView的reload操作。 当WebContent process即将白屏时会触发该回调。 此时,wkWebView的url尚未变成nil,所以reload可以生效。 而在高内存消耗的界面可能会频繁reload。
添加一个web content process崩溃的标记位 在崩溃的时候webViewWebContentProcessDidTerminate设置该标记位,在viewWillAppear中根据该标记位来判断,然后reload操作。
通过webView.url来判断
在viewWillAppear的时候对url进行判断,若为空,则reload。
同时使用KVO来监听URL为空的情况,
if ([self.webView.scrollView.superview isKindOfClass:[WKWebView class]]) {
WKWebView *wkWebView = (WKWebView *)(self.webView.scrollView.superview);
[wkWebView addObserver:self forKeyPath:@"URL" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"URL"]) {
NSURL *newUrl = change[@"new"];
NSURL *oldUrl = change[@"old"];
if ([newUrl isEqual:[NSNull null]] && ![oldUrl isEqual:[NSNull null]]) {
[self.webView reload];
}
}
}
通过webView.title来判断
在viewWillAppear的时候对title进行判断,若为空,则reload。
并非所有白屏的情况都会调用上边的回调方法。 如在H5界面present出来一个相机拍照,导致web content挂掉,则返回H5界面的时候,wkwebview的title会为空。 若拍照耗了太多内存,导致内存紧张,WebContent process挂掉, 但上边的回调方法webViewWebContentProcessDidTerminate并未被调用。 白屏时webView的title会被置空,所以可以在viewWillAppear中检测webView.title是否为空来reload界面。 不懂?
通过innerHTML来判断
在viewWillAppear的时候对innerHTML进行判断,若为空,则reload。
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
__weak typeof(self) weakSelf = self;
[self.webView evaluateJavaScript:@"document.querySelector('body').innerHTML" completionHandler:^(id result, NSError *error) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (!result || ([result isKindOfClass:[NSString class]] && [((NSString *)result) length] == 0)) {
NSLog(@"--- H5页面加载异常,重新加载中 -----");
// reload your page
[strongSelf.webView reload];
} else {
NSLog(@"--- H5页面加载正常 -----");
}
}];
}
Cookie问题
问题在于:
WKWebView发起的请求不会自动带上存储于NSHTTPCookieStorage中的cookie。 而UIWebView会自动带上。
Cookie格式:
name=Nicholas;value=test;domain=y.qq.com;expires=Sat, 02 May 2019 23:38:25 GMT;
UIWebView设置cookie
var props = Dictionary<HTTPCookiePropertyKey, Any>()
props[HTTPCookiePropertyKey.name] = "tigerobocookie"
props[HTTPCookiePropertyKey.value] = cookieValue // 一串key-value,以;间隔。
props[HTTPCookiePropertyKey.path] = "/"
props[HTTPCookiePropertyKey.domain] = API.tigerResearchWebDomain // 必须设置souyanbao.tigerobo.com
if let cookie = HTTPCookie(properties: props) {
NSHTTPCookieStorage.shared.setCookie(cookie)
}
xxx
let request = URLRequest(url: url)
webView.loadRequest(request)
WKWebView设置cookie
而WKWebView使用NSHTTPCookieStorage则不行。
使用WKWebView.configuration.websiteDataStore.httpCookieStore
guard let cookies = HTTPCookie(properties: props) else { return }
let httpCookieStore = wkWebView.configuration.websiteDataStore.httpCookieStore
httpCookieStore.setCookie(cookie) {
DispatchQueue.main.async {
self.wkWebView.load(request)
}
}
但是httpCookieStore只在iOS 11之后才能用。
在loadRequest之前,在request header中设置cookie,解决首个cookie带不上的问题
NSURL *url = [NSURL URLWithString:@"http://github.com"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"];
WKWebView *webView = [WKWebView new];
[webView loadRequest:request];
通过document.cookie设置cookie解决后续页面(同域)Ajax,iframe请求的cookie问题
注意:document.cookie()无法跨域设置cookie。
WKUserContentController *userContentController = [WKUserContentController new];
WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:@"document.cookie='skey=skeyValue';" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:cookieScript];
无法解决302请求的cookie问题。 可以在webView:decidePolicyForNavigationAction:decisionHandler:回调方法中拦截302请求,copy request, 在request header中带上cookie并重新loadRequest。不过依然解决不了页面iframe跨越请求的cookie问题。 因为loadRequest只适合加载mainFrame请求。
NSURLProtocol可以用于中间人攻击
WKWebView独立于app之外的进程(webContent process)执行网络请求,请求数据不经过主进程。 所以WKWebView中无法直接使用NSURLProtocol来拦截请求,
loadRequest问题
WKWebView通过loadRequest发起的post请求body数据会丢失。 因为进程间通信性能问题,HTTPBody字段被丢弃
[request setHTTPMethod:@"POST"];
[request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]];
[wkWebView loadRequest:request];
由此想到的
小程序开发中通过的性能优化关键在于setData方法的调用。为啥会这么影响性能呢?
小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。
当前,视图层和逻辑层的数据传输,实际上通过两边提供的evaluateJavascript所实现。即用户传输的数据(由视图层去往逻辑层),需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。
而evaluateJavascript的执行会受很多方面的影响,数据到达视图层并不是实时的。同一进程内的 WebView 实际上会共享一个 JS VM,如果 WebView 内 JS 线程正在执行渲染或其他逻辑,会影响 evaluateJavascript 脚本的实际执行时间,
另外多个 WebView 也会抢占 JS VM 的执行权限;另外还有 JS 本身的编译执行耗时,都是影响数据传输速度的因素。
常见的 setData 错误操作:
- 频繁的去执行setData。 Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重;渲染有出现延时。
- 每次 setData 都传递大量新数据。
- 后台态页面进行 setData。
为何一定要使用setData这种方式:
因为js语言的限制, 不能检测到对象属性的添加或删除. 不能检测到单独对data做修改的操作.