华少的博客

谁的头顶上没有灰尘,谁的肩上没有过齿痕

0%

WKWebview Tips

WKWebview 踩坑之旅

前言

关于iOS 8后出来的WKWebview的简介,及对比UIWebview的优势 就不再做赘述了。可以参考
nshipster介绍WKWebview
学习WKWebview需研究源码,可以调试下miniBrowser项目

背景

在WKWebview出来这几年后,老项目一直未迁移到wkwebview,尽管我们知道它有着60 fps滚动刷新率,内存占用少,和safari相同的JavaScript引擎等优势,但由于其本身的不完善和一些坑点以及迁移的工作量等问题,一直未深入研究迁移方案。
但多个UIWebview浏览的内存暴涨导致容易crash一直是块心病。于是还是尝试迁移到wkwebview,便有了一段踩坑之旅

WKWebview使用注意点

网上也已经有不少使用注意点的总结文章,在此先贴出了,然后讲些自己遇到的问题和处理方案。
WKWebView 那些坑
WKWebviewTips

1. 加载本地html注意区分系统版本

iOS9及以上版本可以使用
[self.webview loadRequest:request]

本地资源生成的request在iOS8下无法加载
iOS8下加载本地文件: [self.webview loadHTMLString:str]
http://stackoverflow.com/questions/27803341/swift-wkwebview-loading-local-file-not-working-on-a-device

2.跨域跳转处理

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
此代理中对于一些navigationAction.navigationType跳转类型判断需要注意自己处理。不然容易出现点击页面链接无响应。

3.共享cookie问题

如果有同时开多个网页的需求,这就需要注意共享cookie了。由于本身WKWebview中使用到了WKProcessPool,导致多个wkwebview间无法共享cookie。查看firefox源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// A WKWebViewConfiguration used for normal tabs
lazy fileprivate var configuration: WKWebViewConfiguration = {
let configuration = WKWebViewConfiguration()
configuration.processPool = WKProcessPool()
configuration.preferences.javaScriptCanOpenWindowsAutomatically = !(self.prefs.boolForKey("blockPopups") ?? true)
return configuration
}()
// A WKWebViewConfiguration used for private mode tabs
lazy fileprivate var privateConfiguration: WKWebViewConfiguration = {
let configuration = WKWebViewConfiguration()
configuration.processPool = WKProcessPool()
configuration.preferences.javaScriptCanOpenWindowsAutomatically = !(self.prefs.boolForKey("blockPopups") ?? true)
configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent()
return configuration
}()

以上代码是firefox在正常模式及隐私模式下创建的configuration.
当然,在app被kill掉后再启动又会被重置,导致pool中的cookie数据丢失。如果不是做浏览器,而是自身app与服务器打交道,可以考虑把cookie信息在请求前手动加上header中。
参考:
http://stackoverflow.com/questions/39772007/wkwebview-persistent-storage-of-cookies

http://stackoverflow.com/questions/26573137/can-i-set-the-cookies-to-be-used-by-a-wkwebview

http://stackoverflow.com/questions/33156567/getting-all-cookies-from-wkwebview

4.部分特殊页面点击链接事件失效

在浏览部分邮箱页面()在新标签页打开 ( wkwebview )时
//例如:target = “_blank”

<a href="https://m.baidu.com/?tn=&amp;from=1018225b" rel="noopener noreferrer" target="_blank" class="">https://m.baidu.com/?tn=&amp;from=1018225b</a>

点击链接后,在这个- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures代理中收到的request.url为空,无法获取到正确请求。
这个问题在firefox上也有且未解决,然而Safari上都是正常的(苹果还是老奸巨猾啊。。)
参考: WKWebview-target-blank-quirks
当然UIWebview也有这样的问题,莫名其妙在CreateNewWebview代理中获取到的request为空,不过,UIWebview可以通过一些私有方式,很方便的获取到网页内部信息。

5.页面跳转后再返回不会执行script和Ajax

从页面a进入页面b再返回页面a后不会重新执行script和Ajax,也不会触发页面reload,此部分情况需要针对实际场景做处理,手动reload。

6.数据清除

iOS 8下WKWebsiteDataStore还不支持,因此需要手动删除Cookies,Caches,Webkit文件夹(注意数据保存的路径问题)
iOS 9虽然有WKWebsiteDataStore接口,但实际使用起来会有概率性crash情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
NSSet *websiteDataTypes
= [NSSet setWithArray:@[
WKWebsiteDataTypeDiskCache,
WKWebsiteDataTypeOfflineWebApplicationCache,
WKWebsiteDataTypeMemoryCache,
WKWebsiteDataTypeLocalStorage,
WKWebsiteDataTypeCookies,
WKWebsiteDataTypeSessionStorage,
WKWebsiteDataTypeIndexedDBDatabases,
WKWebsiteDataTypeWebSQLDatabases
]];
//// All kinds of data
//NSSet *websiteDataTypes = [WKWebsiteDataStore allWebsiteDataTypes];
//// Date from
NSDate *dateFrom = [NSDate dateWithTimeIntervalSince1970:0];
//// Execute
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes modifiedSince:dateFrom completionHandler:^{
// Done
}];

因此建议直接根据路径删除文件夹(iOS 8和iOS 9后数据保存的路径不一致,需要判断)

7.横竖屏切换调整wkwebview视图

在测试时发现WKWebview在iPad上浏览部分网页时,横竖屏切换显示错位,需要重新调整frame。参考firefox代码可见在iOS8以后有VC横竖屏切换开始及完成的代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
displayedPopoverController?.dismiss(animated: true) {
self.displayedPopoverController = nil
}
coordinator.animate(alongsideTransition: { context in
self.scrollController.updateMinimumZoom()
self.topTabsViewController?.scrollToCurrentTab(false, centerCell: false)
if let popover = self.displayedPopoverController {
self.updateDisplayedPopoverProperties?()
self.present(popover, animated: true, completion: nil)
}
}, completion: { _ in
self.scrollController.setMinimumZoom()
})
}

在VC将要开始转屏时,更新保存scrollController中的WKWebview的scrollview.zoomScale和scrollview.zoomScale.minimumZoomScale,然后在转屏完成后设置

1
2
3
if self.isZoomedOut && roundNum(scrollView.zoomScale) != roundNum(scrollView.minimumZoomScale) {
scrollView.zoomScale = scrollView.minimumZoomScale
}

这样达到在转屏时顺畅的调整WKWeview的内容显示。

8.WKWebview刷新机制

在一些低端机型上,滑动webview时明显就一段一段的白屏出现,跟UIWebview表现差远了。这引起了兴趣,查看并研究了下源码,发现wkwebview并不是跟uiwebview一样渲染所有网络数据,而是一屏一屏的渲染,也是这样才有了内存占用少的优势。也有一篇源码的文章推荐下WKWebView刷新机制小探

WKContentView就是WKWebView内容渲染的容器。在Reveal的树状图上面可以看到,渲染页面中,展示在页面上的渲染单元是WKCompositingView,WKCompositingView可以嵌套WKCompositingView。其中的一个WKCompositingView实例,将包含多个WKCompositingView子实例。类似于UITableView的重用机制,多个WKCompositingView的父View就相当于UITableView,WKCompositingView就相当于UITableViewCell,只展示可视区域的内容,达到性能优化的目的。

一个WKWebView加载的web内容,切割成多个WKCompositingView,单个WKCompositingView重用单元的面积是375x512点。

从源码入手确实可以看到很多实现细节,但真正想要想像UIWebview一样hook掉很多系统属性和方法是不可能的,只能说苹果做的真绝。。

理解了这个刷新机制后,在做一些类似新闻类,有多个wkwebview当做uiview add到tableview上,即使竖向滚动也可以绑定wkwebview的scrollview的刷新渲染了。([WKWebView _updateVisibleContentRects]
推荐的文章最后说使用以下3个方法解决白屏问题:

  • 用KVO方法监听UITableView的contnetOffset属性,contentOffset发生变化也就是说UITableView发生滚动,调用WKWebView实例的_updateVisibleContentRects,刷新需要渲染的内容
  • UITableView是继承自UIScrollView的,在代码中实现UIScrollView的delegate,在delegate实现中手动调用WKWebView实例等UIScrollViewDelegate的方法,原理和第一种方法一样
  • 使用CADisplayLink类,在CADisplayLink的回调方法里面调用WKWebView实例的_updateVisibleContentRects即可

真正操作时你会发现,在快速滑动UITableView时,代理暴露出来的点跨度很大,完全不连贯,导致刷新内部webview的内容不及时。因此推荐使用CADisplaylink类,利用屏幕刷新机制去触发wkwebview实例的刷新。

wkwebview致命缺陷 - NSURLProtocol问题

在做浏览器开发时,用户需求比较大的功能就是广告拦截。在这个广告遍地飞的时代,浏览器在用户使用时能自动过滤掉广告显示,将获得好评。

首先简单说明下浏览器广告拦截的几种方式:1.网页开始http请求时,判断url请求是不是广告请求(需要库)2.网页加载完后,判断资源是否需要隐藏;
因此我们需要获取到wkwebview加载网页时的网络请求,针对各个网络请求判断是否进行拦截。
在UIWebview中我们可以使用NSURLProtocol进行拦截,也可以hook系统代理方法- (NSURLRequest *)webView:(id)sender resource:(id)identifier willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse fromDataSource:(id)dataSource
进行拦截js广告。

然而在WKWebview中,WKWebview在独立于app进程之外的进程中执行网络请求,请求数据不经过主进程,因此在WKWebview上直接使用NSURLProtocol无法拦截请求。

不过我们也通过下载查看webkit2源码,发现+ [WKBrowsingContextController registerSchemeForCustomProtocol:]注册自定义的Protocol也是可以拦截请求的。
随之马上发现已这样的方式拦截请求再重新发送时,post形式的请求中body数据被清空了(例如百度登录过程中用户信息丢失无法登录)。

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

遇到此问题后也陆续寻找了各种方案,后续专门再总结出来。

  • 鉴于此,目前就通过WKWebview自身的一些请求代理进行拦截以及在网页加载完成后进行部分css样式或者网页节点拦截。

参考文章: