iOS面试- 0x02 WebView

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

有了UIWebView为什么还需要WKWebView?

UIWebVieW的缺点 笨重难用、内存泄露、内存消耗大性能差 —— WKWebView提高性能 WKWebView 拥有60fps滚动刷新率和safari相同的js引擎等优势。


1、WKWebView 白屏问题

WKWebView是一个多进程的组件Network Loading以及UI Rendering在其他进程中执行。初次适配WKWebView的时候我们也惊讶于打开WKWebView后 App进程内存消耗反而大幅度下降但仔细观察会发现Other Process的内存占用会增加。在一些用webGL渲染的复杂页面使用WKWebView总体的内存占用【App process Memory + other Process Memory】不见得比UIWebView少很多。

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

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

解决方案
《1》借助WKNavigationDelegate
iOS9之后增加的回调函数

/*! @abstract Invoked when the web view's web content process is terminated.
 @param webView The web view whose underlying web content process was terminated.
 */
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macos(10.11), ios(9.0));

在WKWebView总体内存占用过大的时候页面即将出现白屏在上面这个系统回调方法中执行[webview reload]来解决白屏问题。

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

2、WKWebView Cookie问题

2.1、WKWebView Cookie存储

业界普遍认为 WKWebView 拥有自己的私有存储不会将 Cookie 存入到标准的 Cookie 容器 NSHTTPCookieStorage 中。

实践发现 WKWebView 实例其实也会将 Cookie 存储于 NSHTTPCookieStorage 中但存储时机有延迟在iOS 8上当页面跳转的时候当前页面的 Cookie 会写入 NSHTTPCookieStorage 中而在 iOS 10 上JS 执行 document.cookie 或服务器 set-cookie 注入的 Cookie 会很快同步到 NSHTTPCookieStorage 中FireFox 工程师曾建议通过 reset WKProcessPool 来触发 Cookie 同步到 NSHTTPCookieStorage 中实践发现不起作用并可能会引发当前页面 session cookie 丢失等问题。

WKWebView Cookie 问题在于 WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的 Cookie。

比如NSHTTPCookieStorage 中存储了一个 Cookie:

name=Nicholas;value=test;domain=y.qq.com;expires=Sat, 02 May 2019 23:38:25 GMT;

通过 UIWebView 发起请求http://y.qq.com 则请求头会自动带上 cookie: Nicholas=test; 而通过 WKWebView发起请求http://y.qq.com 请求头不会自动带上 cookie: Nicholas=test。

2.2、WKProcessPool

*WKProcessPool定义A WKProcessPool object represents a pool of Web Content process。

通过让所有 WKWebView 共享同一个 WKProcessPool 实例可以实现多个 WKWebView 之间共享 Cookie(session Cookie and persistent Cookie数据。 不过WKWebView WkProcessPool实例在app杀进程重启后会被重置导致WKProcessPool 中的cookie/session Cookie数据丢失目前也无法实现WKProcessPool实例本地化保存。

2.3 Workround

H5的业务都是依赖于Cookie作登陆态校验而WKWebView上请求不会自动携带Cookie目前的主要解决方案是

《1》WKWebView loadRequest前在request header中设置Cookie解决首个请求Cookie带不上的问题

WKWebView * webView = [WKWebView new]; 
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://h5.qzone.qq.com/mqzone/index"]]; 

[request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"]; 
[webView loadRequest:request];

《2》通过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问题比如第一个请求时www.a.com,我们通过在request header里带上Cookie解决该请求的Cookie问题接着页面302跳转到www.b.com, 这个时候www.b.com 这个请求就可能因为没有携带cookie而无法访问。当然由于每一次页面跳转都会调用回调函数

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

可以在该回调函数里拦截302请求copy request 在request header中带上cookie并重新loadRequest。不过这种方法依然解决不了页面的iframe跨域请求的cookie问题毕竟-[WKWebView loadRequest]只适合加载mainiFrame请求。

3、WKWebView NSURLProtocol 问题

WKWebView在独立于App进程之外的进程中执行网络请求请求数据不经过主进程因此在WKWebView上直接使用NSURLProcol无法拦截请求。

苹果开源的WebKit源码暴露了私有API
+ [WKBrowsingContextController registerSchemeForCustomProtocol:]

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

Class cls = NSClassFromString(@"WKBrowsingContextController”); 
SEL sel = NSSelectorFromString(@"
registerSchemeForCustomProtocol:"); 
if ([(id)cls respondsToSelector:sel]) { 
           // 注册http(s) scheme, 把 http和https请求交给 NSURLProtocol处理 
           [(id)cls performSelector:sel withObject:@"
http"]; 
           [(id)cls performSelector:sel withObject:@"
https"]; 
}

缺点 《1》post请求body数据被清空 由于WKWebView在独立进程里网络请求。一旦注册http(s) scheme后网络请求将从Network process发送到App Process这样NSURLProtocol才能拦截网络请求。 在webkit2的设计里使用了messageQueue进行进程之间的通信 Network Process会将请求encode成一个Message然后通过IPC发送给App Process。 出于性能的原因encode的时候HTTPBody和HTTPBodyStream这两个字段丢弃掉了。

因此如果通过registerSchemeForCustomProcol注册了http(s) scheme那么由WKWebView发起的所有http(s)请求都会通过IPC传给进程NSURLProtocol处理导致post请求body被清空;

《2》对ATS支持不足 打开ATS开关 Allow Arbitrary Loads选项设置为NO同时通过registerSchemeForCustomProtocol注册了http(s) scheme;WKWebView发起的所有http网络请求将被阻塞(即便将Allow Arbitrary Loads in Web Content 选项设置为YES) WKWebView可以注册customScheme比如dynamic://因此希望使用离线功能又不使用post方式的请求可以通过customScheme发起请求egdynamic://www.dynamicalbumlocalimage.com/然后在 app 进程 NSURLProtocol 拦截这个请求并加载离线数据。不足 使用post方式的请求该方案依然不适用同时需要H5侧修改请求scheme以及CSP规则。

4、WKWebView loadRequest问题

在WKWebView上通过loadRequest发起的post请求body数据会丢失;

//同样是由于进程间通信性能问题HTTPBody字段被丢弃
[request setHTTPMethod:@"POST"];
[request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]];
[wkwebview loadRequest: request];

workround: 假设想通过-[WKWebView loadRequest:]加载post请求request1 http://h5.qzone.qq.com/mqzone/index可以通过以下步骤实现
1、替换请求scheme生成新的post请求request2post://h5.qzone.qq.com/mqzone/index同时将request1的body字段复制到request2的header中(WebKit不会丢弃header字段
2、通过-[WKWebView loadRequest:]加载新的post请求request2;
3、通过 +[WKbrowsingContextController registerSchemeForCustom Protocol]注册scheme:post://;
4、注册 NSURLProtocol 拦截请求post://h5.qzone.qq.com/mqzone/index ,替换请求 scheme, 生成新的请求 request3: http://h5.qzone.qq.com/mqzone/index将 request2 header的body 字段复制到 request3 的 body 中并使用 NSURLConnection 加载 request3最后通过 NSURLProtocolClient 将加载结果返回 WKWebView;

5、WKWebView页面样式问题

适配过程中发现h5页面元素位置向下偏移或被拉伸变形追踪后发现主要是h5页面高度值异常导致

《1》空间h5页面有透明导航、透明导航下拉刷新、全屏等需求因此之前webView整个是从(0,0开始布局通过调整 首页 新闻 博问 专区 闪存 班级 代码改变世界 搜索 注册 登录 NSSong WKWebView 那些坑 导语 WKWebView 是苹果在 WWDC 2014 上推出的新一代 webView 组件用以替代 UIKit 中笨重难用、内存泄漏的 UIWebView。WKWebView 拥有60fps滚动刷新率、和 safari 相同的 JavaScript 引擎等优势。

简单的适配方法本文不再赘述主要来说说适配 WKWebView 过程中填过的坑以及善待解决的技术难题。

1、WKWebView 白屏问题 WKWebView 自诩拥有更快的加载速度更低的内存占用但实际上 WKWebView 是一个多进程组件Network Loading 以及 UI Rendering 在其它进程中执行。初次适配 WKWebView 的时候我们也惊讶于打开 WKWebView 后App 进程内存消耗反而大幅下降但是仔细观察会发现Other Process 的内存占用会增加。在一些用 webGL 渲染的复杂页面使用 WKWebView 总体的内存占用(App Process Memory + Other Process Memory不见得比 UIWebView 少很多。

在 UIWebView 上当内存占用太大的时候App Process 会 crash;而在 WKWebView 上当总体的内存占用比较大的时候WebContent Process 会 crash从而出现白屏现象。在 WKWebView 中加载下面的测试链接可以稳定重现白屏现象:

http://people.mozilla.org/~rnewman/fennec/mem.html

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

我们最后的解决方案是

A、借助 WKNavigtionDelegate iOS 9以后 WKNavigtionDelegate 新增了一个回调函数

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

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

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

2、WKWebView Cookie 问题 Cookie 问题是目前 WKWebView 的一大短板

2.1、WKWebView Cookie存储 业界普遍认为 WKWebView 拥有自己的私有存储不会将 Cookie 存入到标准的 Cookie 容器 NSHTTPCookieStorage 中。

实践发现 WKWebView 实例其实也会将 Cookie 存储于 NSHTTPCookieStorage 中但存储时机有延迟在iOS 8上当页面跳转的时候当前页面的 Cookie 会写入 NSHTTPCookieStorage 中而在 iOS 10 上JS 执行 document.cookie 或服务器 set-cookie 注入的 Cookie 会很快同步到 NSHTTPCookieStorage 中FireFox 工程师曾建议通过 reset WKProcessPool 来触发 Cookie 同步到 NSHTTPCookieStorage 中实践发现不起作用并可能会引发当前页面 session cookie 丢失等问题。

WKWebView Cookie 问题在于 WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的 Cookie。

比如NSHTTPCookieStorage 中存储了一个 Cookie:

name=Nicholas;value=test;domain=y.qq.com;expires=Sat, 02 May 2019 23:38:25 GMT; 通过 UIWebView 发起请求http://y.qq.com 则请求头会自动带上 cookie: Nicholas=test; 而通过 WKWebView发起请求http://y.qq.com 请求头不会自动带上 cookie: Nicholas=test。

2.2、WKProcessPool 苹果开发者文档对 WKProcessPool 的定义是A WKProcessPool object represents a pool of Web Content process. 通过让所有 WKWebView 共享同一个 WKProcessPool 实例可以实现多个 WKWebView 之间共享 Cookie(session Cookie and persistent Cookie数据。不过 WKWebView WKProcessPool 实例在 app 杀进程重启后会被重置导致 WKProcessPool 中的 Cookie、session Cookie 数据丢失目前也无法实现 WKProcessPool 实例本地化保存。

2.3、Workaround 由于许多 H5 业务都依赖于 Cookie 作登录态校验而 WKWebView 上请求不会自动携带 Cookie, 目前的主要解决方案是

a、WKWebView loadRequest 前在 request header 中设置 Cookie, 解决首个请求 Cookie 带不上的问题; WKWebView * webView = [WKWebView new]; NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://h5.qzone.qq.com/mqzone/index"]];

[request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"]; [webView loadRequest:request]; b、通过 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 问题比如第一个请求是 www.a.com我们通过在 request header 里带上 Cookie 解决该请求的 Cookie 问题接着页面302跳转到 www.b.com这个时候 www.b.com 这个请求就可能因为没有携带 cookie 而无法访问。当然由于每一次页面跳转前都会调用回调函数

  • (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler; 可以在该回调函数里拦截302请求copy request在 request header 中带上 cookie 并重新 loadRequest。不过这种方法依然解决不了页面 iframe 跨域请求的 Cookie 问题毕竟-[WKWebView loadRequest:]只适合加载 mainFrame 请求。

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

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

Class cls = NSClassFromString(@"WKBrowsingContextController”); SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); if ([(id)cls respondsToSelector:sel]) { // 注册http(s) scheme, 把 http和https请求交给 NSURLProtocol处理 [(id)cls performSelector:sel withObject:@"http"]; [(id)cls performSelector:sel withObject:@"https"]; } 但是这种方案目前存在两个严重缺陷

a、post 请求 body 数据被清空 由于 WKWebView 在独立进程里执行网络请求。一旦注册 http(s) scheme 后网络请求将从 Network Process 发送到 App Process这样 NSURLProtocol 才能拦截网络请求。在 webkit2 的设计里使用 MessageQueue 进行进程之间的通信Network Process 会将请求 encode 成一个 Message,然后通过 IPC 发送给 App Process。出于性能的原因encode 的时候 HTTPBody 和 HTTPBodyStream 这两个字段被丢弃掉了

参考苹果源码

https://github.com/WebKit/webkit/blob/fe39539b83d28751e86077b173abd5b7872ce3f9/Source/WebKit2/Shared/mac/WebCoreArgumentCodersMac.mm#L61-L88 (复制链接到浏览器中打开

及bug report:

https://bugs.webkit.org/show_bug.cgi?id=138169 (复制链接到浏览器中打开

因此如果通过 registerSchemeForCustomProtocol 注册了 http(s) scheme, 那么由 WKWebView 发起的所有 http(s)请求都会通过 IPC 传给主进程 NSURLProtocol 处理导致 post 请求 body 被清空;

b、对ATS支持不足 测试发现一旦打开ATS开关Allow Arbitrary Loads 选项设置为NO同时通过 registerSchemeForCustomProtocol 注册了 http(s) schemeWKWebView 发起的所有 http 网络请求将被阻塞(即便将Allow Arbitrary Loads in Web Content 选项设置为YES;

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

4、WKWebView loadRequest 问题 在 WKWebView 上通过 loadRequest 发起的 post 请求 body 数据会丢失

//同样是由于进程间通信性能问题HTTPBody字段被丢弃 [request setHTTPMethod:@"POST"]; [request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]]; [wkwebview loadRequest: request]; workaround: 假如想通过-[WKWebView loadRequest:]加载 post 请求 request1: http://h5.qzone.qq.com/mqzone/index,可以通过以下步骤实现

替换请求 scheme生成新的 post 请求 request2: post://h5.qzone.qq.com/mqzone/index, 同时将 request1 的 body 字段复制到 request2 的 header 中(WebKit 不会丢弃 header 字段;

通过-[WKWebView loadRequest:]加载新的 post 请求 request2;

通过 +[WKBrowsingContextController registerSchemeForCustomProtocol:]注册 scheme: post://;

注册 NSURLProtocol 拦截请求post://h5.qzone.qq.com/mqzone/index ,替换请求 scheme, 生成新的请求 request3: http://h5.qzone.qq.com/mqzone/index将 request2 header的body 字段复制到 request3 的 body 中并使用 NSURLConnection 加载 request3最后通过 NSURLProtocolClient 将加载结果返回 WKWebView;

5、WKWebView 页面样式问题 在 WKWebView 适配过程中我们发现部分H5页面元素位置向下偏移或被拉伸变形追踪后发现主要是H5页面高度值异常导致

1.问题 空间H5页面有透明导航、透明导航下拉刷新、全屏等需求因此之前 webView 整个是从(0, 0开始布局通过调整webView.scrollView.contentInset 来适配特殊导航栏需求。而在 WKWebView 上对 contentInset 的调整会反馈到webView.scrollView.contentSize.height的变化上比如设置 webView.scrollView.contentInset.top = a那么contentSize.height的值会增加a,导致H5页面长度增加页面元素位置向下偏移;

解决方案是调整WKWebView布局方式避免调整webView.scrollView.contentInset。实际上即便在 UIWebView 上也不建议直接调整webView.scrollView.contentInset的值这确实会带来一些奇怪的问题。如果某些特殊情况下非得调整 contentInset 不可的话可以通过下面方式让H5页面恢复正常显示

/**设置contentInset值后通过调整webView.frame让页面恢复正常显示 
 *参考http://km.oa.com/articles/show/277372
 */ 
webView.scrollView.contentInset = UIEdgeInsetsMake(a, 0, 0, 0); 
webView.frame = CGRectMake(webView.frame.origin.x, webView.frame.origin.y, webView.frame.size.width, webView.frame.size.height - a);

2、接入now直播问题 在接入 now 直播的时候我们发现在 iOS 9 上 WKWebView 会出现页面被拉伸变形的情况最后发现是window.innerHeight值不准确导致(在WKWebView上返回了一个非常大的值而H5同学通过获取window.innerHeight来设置页面高度导致页面整体被拉伸。通过查阅相关资料发现这个bug只在 iOS 9 的几个系统版本上出现苹果后来fix了这个bug。我们最后的解决方案是延迟调用window.innerHeight。

setTimeout(function(){height = window.innerHeight},0);
或者
Use shrink-to-fit meta-tag 
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1, shrink-to-fit=no">

6、WKWebView截屏问题

空间玩吧H5小游戏有截屏分享的功能WKWebView 下通过 -[CALayer renderInContext:]实现截屏的方式失效需要通过以下方式实现截屏功能

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

@end

首页 新闻 博问 专区 闪存 班级 代码改变世界 搜索 注册 登录 NSSong WKWebView 那些坑 导语 WKWebView 是苹果在 WWDC 2014 上推出的新一代 webView 组件用以替代 UIKit 中笨重难用、内存泄漏的 UIWebView。WKWebView 拥有60fps滚动刷新率、和 safari 相同的 JavaScript 引擎等优势。

简单的适配方法本文不再赘述主要来说说适配 WKWebView 过程中填过的坑以及善待解决的技术难题。

1、WKWebView 白屏问题 WKWebView 自诩拥有更快的加载速度更低的内存占用但实际上 WKWebView 是一个多进程组件Network Loading 以及 UI Rendering 在其它进程中执行。初次适配 WKWebView 的时候我们也惊讶于打开 WKWebView 后App 进程内存消耗反而大幅下降但是仔细观察会发现Other Process 的内存占用会增加。在一些用 webGL 渲染的复杂页面使用 WKWebView 总体的内存占用(App Process Memory + Other Process Memory不见得比 UIWebView 少很多。

在 UIWebView 上当内存占用太大的时候App Process 会 crash;而在 WKWebView 上当总体的内存占用比较大的时候WebContent Process 会 crash从而出现白屏现象。在 WKWebView 中加载下面的测试链接可以稳定重现白屏现象:

http://people.mozilla.org/~rnewman/fennec/mem.html

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

我们最后的解决方案是

A、借助 WKNavigtionDelegate iOS 9以后 WKNavigtionDelegate 新增了一个回调函数

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

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

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

2、WKWebView Cookie 问题 Cookie 问题是目前 WKWebView 的一大短板

2.1、WKWebView Cookie存储 业界普遍认为 WKWebView 拥有自己的私有存储不会将 Cookie 存入到标准的 Cookie 容器 NSHTTPCookieStorage 中。

实践发现 WKWebView 实例其实也会将 Cookie 存储于 NSHTTPCookieStorage 中但存储时机有延迟在iOS 8上当页面跳转的时候当前页面的 Cookie 会写入 NSHTTPCookieStorage 中而在 iOS 10 上JS 执行 document.cookie 或服务器 set-cookie 注入的 Cookie 会很快同步到 NSHTTPCookieStorage 中FireFox 工程师曾建议通过 reset WKProcessPool 来触发 Cookie 同步到 NSHTTPCookieStorage 中实践发现不起作用并可能会引发当前页面 session cookie 丢失等问题。

WKWebView Cookie 问题在于 WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的 Cookie。

比如NSHTTPCookieStorage 中存储了一个 Cookie:

name=Nicholas;value=test;domain=y.qq.com;expires=Sat, 02 May 2019 23:38:25 GMT; 通过 UIWebView 发起请求http://y.qq.com 则请求头会自动带上 cookie: Nicholas=test; 而通过 WKWebView发起请求http://y.qq.com 请求头不会自动带上 cookie: Nicholas=test。

2.2、WKProcessPool 苹果开发者文档对 WKProcessPool 的定义是A WKProcessPool object represents a pool of Web Content process. 通过让所有 WKWebView 共享同一个 WKProcessPool 实例可以实现多个 WKWebView 之间共享 Cookie(session Cookie and persistent Cookie数据。不过 WKWebView WKProcessPool 实例在 app 杀进程重启后会被重置导致 WKProcessPool 中的 Cookie、session Cookie 数据丢失目前也无法实现 WKProcessPool 实例本地化保存。

2.3、Workaround 由于许多 H5 业务都依赖于 Cookie 作登录态校验而 WKWebView 上请求不会自动携带 Cookie, 目前的主要解决方案是

a、WKWebView loadRequest 前在 request header 中设置 Cookie, 解决首个请求 Cookie 带不上的问题; WKWebView * webView = [WKWebView new]; NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://h5.qzone.qq.com/mqzone/index"]];

[request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"]; [webView loadRequest:request]; b、通过 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 问题比如第一个请求是 www.a.com我们通过在 request header 里带上 Cookie 解决该请求的 Cookie 问题接着页面302跳转到 www.b.com这个时候 www.b.com 这个请求就可能因为没有携带 cookie 而无法访问。当然由于每一次页面跳转前都会调用回调函数

  • (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler; 可以在该回调函数里拦截302请求copy request在 request header 中带上 cookie 并重新 loadRequest。不过这种方法依然解决不了页面 iframe 跨域请求的 Cookie 问题毕竟-[WKWebView loadRequest:]只适合加载 mainFrame 请求。

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

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

Class cls = NSClassFromString(@"WKBrowsingContextController”); SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); if ([(id)cls respondsToSelector:sel]) { // 注册http(s) scheme, 把 http和https请求交给 NSURLProtocol处理 [(id)cls performSelector:sel withObject:@"http"]; [(id)cls performSelector:sel withObject:@"https"]; } 但是这种方案目前存在两个严重缺陷

a、post 请求 body 数据被清空 由于 WKWebView 在独立进程里执行网络请求。一旦注册 http(s) scheme 后网络请求将从 Network Process 发送到 App Process这样 NSURLProtocol 才能拦截网络请求。在 webkit2 的设计里使用 MessageQueue 进行进程之间的通信Network Process 会将请求 encode 成一个 Message,然后通过 IPC 发送给 App Process。出于性能的原因encode 的时候 HTTPBody 和 HTTPBodyStream 这两个字段被丢弃掉了

参考苹果源码

https://github.com/WebKit/webkit/blob/fe39539b83d28751e86077b173abd5b7872ce3f9/Source/WebKit2/Shared/mac/WebCoreArgumentCodersMac.mm#L61-L88 (复制链接到浏览器中打开

及bug report:

https://bugs.webkit.org/show_bug.cgi?id=138169 (复制链接到浏览器中打开

因此如果通过 registerSchemeForCustomProtocol 注册了 http(s) scheme, 那么由 WKWebView 发起的所有 http(s)请求都会通过 IPC 传给主进程 NSURLProtocol 处理导致 post 请求 body 被清空;

b、对ATS支持不足 测试发现一旦打开ATS开关Allow Arbitrary Loads 选项设置为NO同时通过 registerSchemeForCustomProtocol 注册了 http(s) schemeWKWebView 发起的所有 http 网络请求将被阻塞(即便将Allow Arbitrary Loads in Web Content 选项设置为YES;

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

4、WKWebView loadRequest 问题 在 WKWebView 上通过 loadRequest 发起的 post 请求 body 数据会丢失

//同样是由于进程间通信性能问题HTTPBody字段被丢弃 [request setHTTPMethod:@"POST"]; [request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]]; [wkwebview loadRequest: request]; workaround: 假如想通过-[WKWebView loadRequest:]加载 post 请求 request1: http://h5.qzone.qq.com/mqzone/index,可以通过以下步骤实现

替换请求 scheme生成新的 post 请求 request2: post://h5.qzone.qq.com/mqzone/index, 同时将 request1 的 body 字段复制到 request2 的 header 中(WebKit 不会丢弃 header 字段;

通过-[WKWebView loadRequest:]加载新的 post 请求 request2;

通过 +[WKBrowsingContextController registerSchemeForCustomProtocol:]注册 scheme: post://;

注册 NSURLProtocol 拦截请求post://h5.qzone.qq.com/mqzone/index ,替换请求 scheme, 生成新的请求 request3: http://h5.qzone.qq.com/mqzone/index将 request2 header的body 字段复制到 request3 的 body 中并使用 NSURLConnection 加载 request3最后通过 NSURLProtocolClient 将加载结果返回 WKWebView;

5、WKWebView 页面样式问题 在 WKWebView 适配过程中我们发现部分H5页面元素位置向下偏移或被拉伸变形追踪后发现主要是H5页面高度值异常导致

a. 空间H5页面有透明导航、透明导航下拉刷新、全屏等需求因此之前 webView 整个是从(0, 0开始布局通过调整webView.scrollView.contentInset 来适配特殊导航栏需求。而在 WKWebView 上对 contentInset 的调整会反馈到webView.scrollView.contentSize.height的变化上比如设置 webView.scrollView.contentInset.top = a那么contentSize.height的值会增加a,导致H5页面长度增加页面元素位置向下偏移;

解决方案是调整WKWebView布局方式避免调整webView.scrollView.contentInset。实际上即便在 UIWebView 上也不建议直接调整webView.scrollView.contentInset的值这确实会带来一些奇怪的问题。如果某些特殊情况下非得调整 contentInset 不可的话可以通过下面方式让H5页面恢复正常显示

/**设置contentInset值后通过调整webView.frame让页面恢复正常显示 *参考http://km.oa.com/articles/show/277372 */ webView.scrollView.contentInset = UIEdgeInsetsMake(a, 0, 0, 0); webView.frame = CGRectMake(webView.frame.origin.x, webView.frame.origin.y, webView.frame.size.width, webView.frame.size.height - a); b. 在接入 now 直播的时候我们发现在 iOS 9 上 WKWebView 会出现页面被拉伸变形的情况最后发现是window.innerHeight值不准确导致(在WKWebView上返回了一个非常大的值而H5同学通过获取window.innerHeight来设置页面高度导致页面整体被拉伸。通过查阅相关资料发现这个bug只在 iOS 9 的几个系统版本上出现苹果后来fix了这个bug。我们最后的解决方案是延迟调用window.innerHeight

setTimeout(function(){height = window.innerHeight},0); or

Use shrink-to-fit meta-tag 6、WKWebView 截屏问题 空间玩吧H5小游戏有截屏分享的功能WKWebView 下通过 -[CALayer renderInContext:]实现截屏的方式失效需要通过以下方式实现截屏功能

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

@end

然而这种方式依然解决不了 webGL 页面的截屏问题笔者已经翻遍苹果文档研究过 webKit2 源码里的截屏私有API依然没有找到合适的解决方案同时发现 Safari 以及 Chrome 这两个全量切换到 WKWebView 的浏览器也存在同样的问题对webGL 页面的截屏结果不是空白就是纯黑图片。无奈之下我们只能约定一个JS接口让游戏开发商实现该接口具体是通过 canvas getImageData()方法取得图片数据后返回 base64 格式的数据客户端在需要截图的时候调用这个JS接口获取 base64 String 并转换成 UIImage。

7、WKWebView crash问题

WKWebView 放量后外网新增了一些 crash, 其中一类 crash 的主要堆栈如下

... 
28 UIKit 0x0000000190513360 UIApplicationMain + 208 
29 Qzone 0x0000000101380570 main (main.m:181) 
30 libdyld.dylib 0x00000001895205b8 _dyld_process_info_notify_release + 36 
Completion handler passed to -[QZWebController webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:] was not called

主要是JS调用window.alert()函数引起的从 crash 堆栈可以看出是 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 

    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; 
    [alertController addAction:[UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]]; 
    [self presentViewController:alertController animated:YES completion:^{}]; 
}

如果 WKWebView 退出的时候JS刚好执行了window.alert(), alert 框可能弹不出来completionHandler 最后没有被执行导致 crash;另一种情况是在 WKWebView 一打开JS就执行window.alert()这个时候由于 WKWebView 所在的 UIViewController 出现(push或present的动画尚未结束alert 框可能弹不出来completionHandler 最后没有被执行导致 crash。我们最终的实现大致是这样的

- (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(); 
}

确保上面两种情况下 completionHandler 都能被执行消除了 WKWebView 下弹 alert 框的 crashWKWebView 下弹 confirm 框的 crash 的原因与解决方式与 alert 类似。

另一个 crash 发生在 WKWebView 退出前调用

 -[WKWebView evaluateJavaScript: completionHandler:]

执行JS代码的情况下。WKWebView 退出并被释放后导致completionHandler变成野指针而此时 javaScript Core 还在执行JS代码待 javaScript Core 执行完毕后会调用completionHandler()导致 crash。这个 crash 只发生在 iOS 8 系统上参考Apple Open Source在iOS9及以后系统苹果已经修复了这个bug主要是对completionHandler block做了copy(refer: https://trac.webkit.org/changeset/179160;对于iOS 8系统可以通过在 completionHandler 里 retain WKWebView 防止 completionHandler 被过早释放。我们最后用 methodSwizzle hook 了这个系统方法

+ (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); 
        } 
    }]; 
}

其他问题

首页 新闻 博问 专区 闪存 班级 代码改变世界 搜索 注册 登录 NSSong WKWebView 那些坑 导语 WKWebView 是苹果在 WWDC 2014 上推出的新一代 webView 组件用以替代 UIKit 中笨重难用、内存泄漏的 UIWebView。WKWebView 拥有60fps滚动刷新率、和 safari 相同的 JavaScript 引擎等优势。

简单的适配方法本文不再赘述主要来说说适配 WKWebView 过程中填过的坑以及善待解决的技术难题。

1、WKWebView 白屏问题 WKWebView 自诩拥有更快的加载速度更低的内存占用但实际上 WKWebView 是一个多进程组件Network Loading 以及 UI Rendering 在其它进程中执行。初次适配 WKWebView 的时候我们也惊讶于打开 WKWebView 后App 进程内存消耗反而大幅下降但是仔细观察会发现Other Process 的内存占用会增加。在一些用 webGL 渲染的复杂页面使用 WKWebView 总体的内存占用(App Process Memory + Other Process Memory不见得比 UIWebView 少很多。

在 UIWebView 上当内存占用太大的时候App Process 会 crash;而在 WKWebView 上当总体的内存占用比较大的时候WebContent Process 会 crash从而出现白屏现象。在 WKWebView 中加载下面的测试链接可以稳定重现白屏现象:

http://people.mozilla.org/~rnewman/fennec/mem.html

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

我们最后的解决方案是

A、借助 WKNavigtionDelegate iOS 9以后 WKNavigtionDelegate 新增了一个回调函数

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

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

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

2、WKWebView Cookie 问题 Cookie 问题是目前 WKWebView 的一大短板

2.1、WKWebView Cookie存储 业界普遍认为 WKWebView 拥有自己的私有存储不会将 Cookie 存入到标准的 Cookie 容器 NSHTTPCookieStorage 中。

实践发现 WKWebView 实例其实也会将 Cookie 存储于 NSHTTPCookieStorage 中但存储时机有延迟在iOS 8上当页面跳转的时候当前页面的 Cookie 会写入 NSHTTPCookieStorage 中而在 iOS 10 上JS 执行 document.cookie 或服务器 set-cookie 注入的 Cookie 会很快同步到 NSHTTPCookieStorage 中FireFox 工程师曾建议通过 reset WKProcessPool 来触发 Cookie 同步到 NSHTTPCookieStorage 中实践发现不起作用并可能会引发当前页面 session cookie 丢失等问题。

WKWebView Cookie 问题在于 WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的 Cookie。

比如NSHTTPCookieStorage 中存储了一个 Cookie:

name=Nicholas;value=test;domain=y.qq.com;expires=Sat, 02 May 2019 23:38:25 GMT; 通过 UIWebView 发起请求http://y.qq.com 则请求头会自动带上 cookie: Nicholas=test; 而通过 WKWebView发起请求http://y.qq.com 请求头不会自动带上 cookie: Nicholas=test。

2.2、WKProcessPool 苹果开发者文档对 WKProcessPool 的定义是A WKProcessPool object represents a pool of Web Content process. 通过让所有 WKWebView 共享同一个 WKProcessPool 实例可以实现多个 WKWebView 之间共享 Cookie(session Cookie and persistent Cookie数据。不过 WKWebView WKProcessPool 实例在 app 杀进程重启后会被重置导致 WKProcessPool 中的 Cookie、session Cookie 数据丢失目前也无法实现 WKProcessPool 实例本地化保存。

2.3、Workaround 由于许多 H5 业务都依赖于 Cookie 作登录态校验而 WKWebView 上请求不会自动携带 Cookie, 目前的主要解决方案是

a、WKWebView loadRequest 前在 request header 中设置 Cookie, 解决首个请求 Cookie 带不上的问题; WKWebView * webView = [WKWebView new]; NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://h5.qzone.qq.com/mqzone/index"]];

[request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"]; [webView loadRequest:request]; b、通过 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 问题比如第一个请求是 www.a.com我们通过在 request header 里带上 Cookie 解决该请求的 Cookie 问题接着页面302跳转到 www.b.com这个时候 www.b.com 这个请求就可能因为没有携带 cookie 而无法访问。当然由于每一次页面跳转前都会调用回调函数

  • (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler; 可以在该回调函数里拦截302请求copy request在 request header 中带上 cookie 并重新 loadRequest。不过这种方法依然解决不了页面 iframe 跨域请求的 Cookie 问题毕竟-[WKWebView loadRequest:]只适合加载 mainFrame 请求。

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

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

Class cls = NSClassFromString(@"WKBrowsingContextController”); SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); if ([(id)cls respondsToSelector:sel]) { // 注册http(s) scheme, 把 http和https请求交给 NSURLProtocol处理 [(id)cls performSelector:sel withObject:@"http"]; [(id)cls performSelector:sel withObject:@"https"]; } 但是这种方案目前存在两个严重缺陷

a、post 请求 body 数据被清空 由于 WKWebView 在独立进程里执行网络请求。一旦注册 http(s) scheme 后网络请求将从 Network Process 发送到 App Process这样 NSURLProtocol 才能拦截网络请求。在 webkit2 的设计里使用 MessageQueue 进行进程之间的通信Network Process 会将请求 encode 成一个 Message,然后通过 IPC 发送给 App Process。出于性能的原因encode 的时候 HTTPBody 和 HTTPBodyStream 这两个字段被丢弃掉了

参考苹果源码

https://github.com/WebKit/webkit/blob/fe39539b83d28751e86077b173abd5b7872ce3f9/Source/WebKit2/Shared/mac/WebCoreArgumentCodersMac.mm#L61-L88 (复制链接到浏览器中打开

及bug report:

https://bugs.webkit.org/show_bug.cgi?id=138169 (复制链接到浏览器中打开

因此如果通过 registerSchemeForCustomProtocol 注册了 http(s) scheme, 那么由 WKWebView 发起的所有 http(s)请求都会通过 IPC 传给主进程 NSURLProtocol 处理导致 post 请求 body 被清空;

b、对ATS支持不足 测试发现一旦打开ATS开关Allow Arbitrary Loads 选项设置为NO同时通过 registerSchemeForCustomProtocol 注册了 http(s) schemeWKWebView 发起的所有 http 网络请求将被阻塞(即便将Allow Arbitrary Loads in Web Content 选项设置为YES;

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

4、WKWebView loadRequest 问题 在 WKWebView 上通过 loadRequest 发起的 post 请求 body 数据会丢失

//同样是由于进程间通信性能问题HTTPBody字段被丢弃 [request setHTTPMethod:@"POST"]; [request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]]; [wkwebview loadRequest: request]; workaround: 假如想通过-[WKWebView loadRequest:]加载 post 请求 request1: http://h5.qzone.qq.com/mqzone/index,可以通过以下步骤实现

替换请求 scheme生成新的 post 请求 request2: post://h5.qzone.qq.com/mqzone/index, 同时将 request1 的 body 字段复制到 request2 的 header 中(WebKit 不会丢弃 header 字段;

通过-[WKWebView loadRequest:]加载新的 post 请求 request2;

通过 +[WKBrowsingContextController registerSchemeForCustomProtocol:]注册 scheme: post://;

注册 NSURLProtocol 拦截请求post://h5.qzone.qq.com/mqzone/index ,替换请求 scheme, 生成新的请求 request3: http://h5.qzone.qq.com/mqzone/index将 request2 header的body 字段复制到 request3 的 body 中并使用 NSURLConnection 加载 request3最后通过 NSURLProtocolClient 将加载结果返回 WKWebView;

5、WKWebView 页面样式问题 在 WKWebView 适配过程中我们发现部分H5页面元素位置向下偏移或被拉伸变形追踪后发现主要是H5页面高度值异常导致

a. 空间H5页面有透明导航、透明导航下拉刷新、全屏等需求因此之前 webView 整个是从(0, 0开始布局通过调整webView.scrollView.contentInset 来适配特殊导航栏需求。而在 WKWebView 上对 contentInset 的调整会反馈到webView.scrollView.contentSize.height的变化上比如设置 webView.scrollView.contentInset.top = a那么contentSize.height的值会增加a,导致H5页面长度增加页面元素位置向下偏移;

解决方案是调整WKWebView布局方式避免调整webView.scrollView.contentInset。实际上即便在 UIWebView 上也不建议直接调整webView.scrollView.contentInset的值这确实会带来一些奇怪的问题。如果某些特殊情况下非得调整 contentInset 不可的话可以通过下面方式让H5页面恢复正常显示

/**设置contentInset值后通过调整webView.frame让页面恢复正常显示 *参考http://km.oa.com/articles/show/277372 */ webView.scrollView.contentInset = UIEdgeInsetsMake(a, 0, 0, 0); webView.frame = CGRectMake(webView.frame.origin.x, webView.frame.origin.y, webView.frame.size.width, webView.frame.size.height - a); b. 在接入 now 直播的时候我们发现在 iOS 9 上 WKWebView 会出现页面被拉伸变形的情况最后发现是window.innerHeight值不准确导致(在WKWebView上返回了一个非常大的值而H5同学通过获取window.innerHeight来设置页面高度导致页面整体被拉伸。通过查阅相关资料发现这个bug只在 iOS 9 的几个系统版本上出现苹果后来fix了这个bug。我们最后的解决方案是延迟调用window.innerHeight

setTimeout(function(){height = window.innerHeight},0); or

Use shrink-to-fit meta-tag 6、WKWebView 截屏问题 空间玩吧H5小游戏有截屏分享的功能WKWebView 下通过 -[CALayer renderInContext:]实现截屏的方式失效需要通过以下方式实现截屏功能

@implementation UIView (ImageSnapshot)

  • (UIImage*)imageSnapshot { UIGraphicsBeginImageContextWithOptions(self.bounds.size,YES,self.contentScaleFactor); [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES]; UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return newImage; } @end 然而这种方式依然解决不了 webGL 页面的截屏问题笔者已经翻遍苹果文档研究过 webKit2 源码里的截屏私有API依然没有找到合适的解决方案同时发现 Safari 以及 Chrome 这两个全量切换到 WKWebView 的浏览器也存在同样的问题对webGL 页面的截屏结果不是空白就是纯黑图片。无奈之下我们只能约定一个JS接口让游戏开发商实现该接口具体是通过 canvas getImageData()方法取得图片数据后返回 base64 格式的数据客户端在需要截图的时候调用这个JS接口获取 base64 String 并转换成 UIImage。

7、WKWebView crash问题 WKWebView 放量后外网新增了一些 crash, 其中一类 crash 的主要堆栈如下

... 28 UIKit 0x0000000190513360 UIApplicationMain + 208 29 Qzone 0x0000000101380570 main (main.m:181) 30 libdyld.dylib 0x00000001895205b8 _dyld_process_info_notify_release + 36 Completion handler passed to -[QZWebController webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:] was not called 主要是JS调用window.alert()函数引起的从 crash 堆栈可以看出是 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 { UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]]; [self presentViewController:alertController animated:YES completion:^{}]; } 如果 WKWebView 退出的时候JS刚好执行了window.alert(), alert 框可能弹不出来completionHandler 最后没有被执行导致 crash;另一种情况是在 WKWebView 一打开JS就执行window.alert()这个时候由于 WKWebView 所在的 UIViewController 出现(push或present的动画尚未结束alert 框可能弹不出来completionHandler 最后没有被执行导致 crash。我们最终的实现大致是这样的

  • (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(); } 确保上面两种情况下 completionHandler 都能被执行消除了 WKWebView 下弹 alert 框的 crashWKWebView 下弹 confirm 框的 crash 的原因与解决方式与 alert 类似。

另一个 crash 发生在 WKWebView 退出前调用

-[WKWebView evaluateJavaScript: completionHandler:] 执行JS代码的情况下。WKWebView 退出并被释放后导致completionHandler变成野指针而此时 javaScript Core 还在执行JS代码待 javaScript Core 执行完毕后会调用completionHandler()导致 crash。这个 crash 只发生在 iOS 8 系统上参考Apple Open Source在iOS9及以后系统苹果已经修复了这个bug主要是对completionHandler block做了copy(refer: https://trac.webkit.org/changeset/179160;对于iOS 8系统可以通过在 completionHandler 里 retain WKWebView 防止 completionHandler 被过早释放。我们最后用 methodSwizzle hook 了这个系统方法

  • (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); } }]; } 8、其它问题 8.1、视频自动播放 WKWebView 需要通过WKWebViewConfiguration.mediaPlaybackRequiresUserAction设置是否允许自动播放但一定要在 WKWebView 初始化之前设置在 WKWebView 初始化之后设置无效。

8.2、goBack API问题 WKWebView 上调用 -[WKWebView goBack], 回退到上一个页面后不会触发window.onload()函数、不会执行JS。

8.3、页面滚动速率 WKWebView 需要通过scrollView delegate调整滚动速率

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
     scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
}

10、App嵌入小段html代码 —— CoreText

alt 1、富文本复杂的排版
2、图片
3、连接

优点
1比webView消耗少
2后台渲染【非常适用于内容排版工作】快
3精确

缺点 1不能够像webView那样支持复制
2需要自己处理很多逻辑

小结

坑多 相对 UIWebView 在内存消耗、稳定性方面还是有很大的优势。


WKWebView开发遇到的一些参考

本文由 mdnice 多平台发布

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6