iOS面试- 0x02 WebView

news2025/4/19 11:06:23

有了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发起请求,eg: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];

workround: 假设想通过-[WKWebView loadRequest:]加载post请求,request1: http://h5.qzone.qq.com/mqzone/index,可以通过以下步骤实现:
1、替换请求scheme,生成新的post请求request2:post://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) scheme,WKWebView 发起的所有 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) scheme,WKWebView 发起的所有 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 框的 crash,WKWebView 下弹 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) scheme,WKWebView 发起的所有 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 框的 crash,WKWebView 下弹 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 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/191976.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ElasticSearch6.x版本的Scroll滚动查询讲解及Kibana和SpringBoot实操演示

文章目录一、Scroll滚动查询介绍二、Kibana上操作三、SpringBoot中操作四、总结一、Scroll滚动查询介绍 ElasticSearch中在进行普通的查询时&#xff0c;默认只会查询出来10条数据。我们通过设置ElasticSearch中的size可以将最终的查询结果从10增加到10000。但这时候如果我们需…

Sensor曝光和帧率基础知识

Sensor曝光和帧率基础知识1. 简介2. H_BLANK和V_BLANK3. 曝光原理3.1 Sensor逐行曝光基本原理3.2 Sensor全局曝光基本原理4. 曝光时间计算公式4.1 曝光一行的时间line_timeline\_timeline_time4.2 曝光一帧的时间exposure_timeexposure\_timeexposure_time4.3 帧率(fps)的计算1…

2023,“蔚小理”真的经不起更多“事故”了

历史总是惊人的相似。2013年&#xff0c;哈弗品牌独立出来&#xff0c;与长城品牌并行运营&#xff0c;当时推出两年的哈弗H6正卖得火热&#xff0c;推动SUV这个细分品类在中国快速成长&#xff0c;中国自主品牌也借SUV开始攻占被合资品牌占领的市场。时移势易。十年后的2023年…

浅析TSINGSEE车载监控平台助力城市公交智能监管的方案设计

道路运输已成为铁路以外最重要的地面运输方式&#xff0c;在国民经济和社会发展中发挥着举足轻重的作用。然而&#xff0c;随着汽车的普及和交通需求的快速增长&#xff0c;道路运输带来的交通拥堵、交通事故和环境污染等负面影响日益突出&#xff0c;逐渐成为全球经济社会发展…

Baklib支招 ——如何帮助企业创建内部维基(wiki)?

企业维基&#xff08;wiki&#xff09;的重要的好处就是&#xff1a;可以按照个性化的理解和需求进行编辑&#xff0c;而不担心被别人修改。这个个人维基可以作为自己的外挂大脑使用&#xff0c;但是不要成为一个垃圾筐&#xff0c;什么都往里装&#xff0c;扔进去就再也不看了…

计算机SCI论文重复率需要控制在多少? - 易智编译EaseEditing

SCI论文的重复率一般在20%左右&#xff0c;一般是没有没问题的。 SCI期刊在检查论文重复率时&#xff0c;并不是简单的只看总重复率&#xff0c;还有单篇重复率。 目前有很多SCI期刊都会先查重&#xff0c;看重复率&#xff0c;但也会看内容。 而且&#xff0c;重复率高也不一定…

lio-sam学习笔记(一)

前言&#xff1a; 对于lio-sam框架的安装配置。 每一回不同框架的配置真是要了老命了。。。 一、安装依赖 官方github&#xff1a; GitHub - TixiaoShan/LIO-SAM: LIO-SAM: Tightly-coupled Lidar Inertial Odometry via Smoothing and Mapping lio-sam主要有两个依赖&am…

6.深度学习和计算

6.深度学习和计算 目录 层和块 自定义块顺序块在前向传播函数中执行代码 参数管理 参数访问 目标参数一次性访问所有参数从嵌套块收集参数 参数初始化 内置初始化自定义初始化 参数绑定 自定义层 不带参数的层带参数的层 读写文件 加载和保存张量加载和保存模型参数使用GPU 计…

JS中函数声明与函数表达式的区别

1、函数定义 JavaScript 中定义函数最常用的方式是函数声明和函数表达式。这两种技术非常相似&#xff0c;有时甚至难以区分&#xff0c;它们之间还是存在着微妙的差别。 JavaScript 定义函数的最基本方式是函数声明&#xff1a; 函数声明必须独立&#xff0c;但也能够被包含在…

REST开发

REST风格一、简介优点REST风格二、使用三、注解PathVariableResponseBody、RequestParam和PathVariable区别应用快速开发一、简介 REST&#xff08;Representational State Transfer&#xff09;&#xff0c;表现形式状态转换 传统风格资源描述形式 http://localhost/user/ge…

Smartbi电子表格软件集成的优势

Smartbi电子表格软件作为国内顶尖的企业报表工具&#xff0c;具有“真Excel”特色&#xff0c;直接用Excel作为设计器&#xff0c;仅需安装一个插件就可以解决众多报表难题。无需增加学习成本&#xff0c;学习一个新的设计器&#xff0c;更能解决Excel的取数、性能等问题&#…

Wireless M-Bus介绍-摘自OMS

Wireless M-Bus(wM-Bus或无线M-Bus)是成熟的Wired M-Bus(有线M-Bus)标准的后续增强。2003年&#xff08;这里估计是资料错误,第一个无线M-Bus的标准是2013年的&#xff09;&#xff0c;EN 13757-4首次对其进行了描述&#xff08;&#xff1f;&#xff09;。该标准为M-Bus层模型…

Python 全栈系列216 APIFunc.Database启动流程

说明 上次做了一些改进,现在关注使用的流程。 内容 1 基础概念 能大约知道大致的几个部分有助于记忆操作步骤,以后这个项目的代号就是ABD了。 1 所有启动相关的代码全部封装在镜像apifunc_database_model1:v3,基于这个镜像就可以启动一个所有对应的服务流2 在Portal上有相关…

为WEB3 的GameFi行业爆发提供全新动力

WEB3 GameFi的突破口在2022年末Octopus Network Co-founder Louis指出&#xff0c;GameFi要完成2023年的全新爆发需要将所有权与经营权分离&#xff0c;所有用户共同参与由平台成长带来的市场红利&#xff0c;单一的Gamefi将会更加产品化&#xff0c;体验化&#xff0c;具备整合…

详解树状数组

前言树状数组或二叉索引树&#xff08;Binary Indexed Tree&#xff09;&#xff0c;又以其发明者命名为 Fenwick 树。其初衷是解决数据压缩里的累积频率的计算问题&#xff0c;现多用于高效计算数列的前缀和、区间和。它可以以 O(logn) 的时间得到任意前缀和。并同时支持在 O(…

JVM线上问题定位命令

概述 本文主要介绍下平常可能会使用到的命令&#xff1a;jps、jinfo、jmap、jstat、jstack jps jps主要是查看Java进程号&#xff0c;有个Java进程号后面的命令也才能发挥作用。 jps -help可以列出jps支持的参数&#xff0c;大家可以试一下 jinfo jinfo后面跟jps打出来的…

nvm(node版本管理)

文章目录1 安装2 配置镜像&#xff0c;提高下载速度3 常用命令4 vue版本与node版本的依赖关系注意事项1 nvm use 报错1 安装 官方下载地址&#xff1a; https://nvm.uihtm.com/ 官网同时还给出了非常详细的指令文档。 下载windows版本的安装包&#xff0c;按照提示点击下一步…

网络通信Socket学习记录

网络通信Socket Socket socket起源于unix&#xff0c;而unix/linux基本思想就是一切皆文件&#xff0c;也称为文件描述符socket是对“open—write/read—close”的一种实现socket是对TCP/IP协议的一种封装&#xff0c;socket本身不是协议&#xff0c;通过socket才能使用TCP/I…

一站式轻监控轻运维系统nezha(上篇)

本文软件由网友 114514 推荐&#xff1b; 什么是哪吒监控 &#xff1f; 哪吒监控 是自托管、轻量级服务器和网站监控运维工具&#xff0c;属于一站式轻监控轻运维系统&#xff0c;分主控端和被控端&#xff0c;主控端提供访问的面板&#xff0c;被控端就是需要监控的服务器。 准…

Python的集合(set)类型、None类型.....

Day07 数据类型&#xff08;下&#xff09; dict,字典类型,一个容器&#xff0c;元素必须是键值对 Set,集合类型&#xff0c;一个不允许重复&可变类型&#xff08;元素可哈希&#xff09; float&#xff0c;浮点类型&#xff0c;常见的小数 文章目录1.集合&#xff08;se…