什么是WebKit?
Apple在2003年的时候发布了第一版Safari,然后再2005年的时候开源了Safari游览器的内核WebKit, 一直到现在Safari游览器的内核都是WebKit,而Google使用的游览器内核是Blink,它是从WebKit拉出的分支,加以优化改进的游览器内核。
而WebKit的核心组件就是:WebCore以及JavaScriptCore。 WebKit框架提供了OC的API接口来访问基于C++的WebCore渲染引擎以及JavaScriptCore脚本引擎,这样应用程序可以通过Cocoa API方便的访问。
而在使用中 ,Apple要求该平台上的Web游览器都使用WebKit,所以iOS版本的Chrome游览器使用的内核也是WebKit, 而桌面端的Chrome则使用的Blink引擎(仅使用了WebCore,以及自己命名为V8的JavaScript引擎和一个多进程系统)众多基于开源的webkit开发的知名游览器,都使用了WebKit的WebCore,但是JavaScript引擎都是自己重写的。
WebCore
WebCore是一个渲染引擎(the rendering engine),主要是将HTML解析为DOM,进行布局以及渲染等等工作。
JavaScriptCore
JavaScriptCoer是WebKit的JavaScript引擎,它是解释执行JavaScript代码的。但是它有好几个不同的名字,在Safari的上下文中,一般称之为Nitro。
WebKit的多线程
WebKit是传统的架构,所有的都在一个进程中,这是一个非常简单的模型,应用程序复用WebKit的API也是非常容易的。
而WebKit2则使用了多进程模型,部分WebKit的操作在UI进程中,应用的逻辑处理也在这个进程。而剩下WebKit,包括WebCore以及JS引擎,都在web进程中工作。这个web process和UI process是隔离的。所以更加安全,更能充分利用多核CPU。
WKWebView的多线程
WKWebView是多进程的机制,查看编译好的WebKit源码可看到一个文件夹目录:
这里面有三个主要的进程:
- UIProcess:也就是App所在的进程,WKWebView在该进程中提供了大量API供开发者与内核交互,也是开发者最熟悉的进程。
- NetworkProcess:无论是多个WKWebView实例还是单个,都只有一个唯一的NetWorkingProcess,主要便于网络请求管理,网络缓存管理,Cookie管理的一致性。
- WebProcess:也就是WebContent进程,主要负责WebCore,JSCore相关模块的运行,是WKWebView的核心进程。每个WKWebView都会有独立的Web进程,不过该进程会根据内存进行复用。某一个webContent进程的异常并不会影响到主进程,常见的异常现象是白屏现象。
\
同时,各个进程之间是通过CorelPC进程来通信的。
总结:在一个客户端中,多个WKWebView实例会共享一个UI进程(和App进程共享),共享一个Networking进程,每个WKWebView实例独享一个Web进程。
WKWebView的Cookies 管理
Cookie的存储
从文档可知,App中的Cookie管理使用的HTTPCookieStrorage,WKWebView的Cookie管理使用的是WKHttpCookieStore。这两个Cookie是可以同步的,但是同步会有明显的延迟,所以为了代码的稳定性,需要我们自己去处理cookie。
在使用UIWebView的时候,它是由HTTPCookieStorage类来管理Cookie,和App进程其他组件读取Cookie时读取的是同一块区域,然而WKWebView的Cookie设计不太一样,因为它和APP不在一个进程,所以它们的Session是不一致的,它们的Cookie也存储在不同的内存区域。
但是这里要注意一点,就是在iOS上App进程持久化的Cookie和WKWebView进程持久化的Cookie确实不在一个文件夹,然而Mac上App进程和WKWebView进程持久化的Cookie都在同一个文件夹!
对于iOS上,Cookie文件有两种:
- bundleID.binarycookies 这是HttpCookieStorage持久化的Cookie
- Cookies.binarycookies 这是WKWebView持久化的Cookie
至于Cache文件,这个确实是存储在不同的文件夹(iOS,iPad,Mac都一样),这里我们实际看看内存文件即可:
关于这一点在Apple的文档中也有描述(共享Cookie内存):
Cookie的类型
实际上我们使用的时候会遇到两种类型的Cookie:
1、Session Cookie
这种类型的Cookie是只对当前进程可知的,并不会引起Cookie共享。也就是说App进程使用的SessionCookie,和WKWebView使用的SessionCookie是不一样的,这就是会导致Cookie不一致的原因。
2、Persistent Cookie
那么当进程杀死之后,如果没有持久化的话(就是保存在Cookie文件夹中),那么下一次进入App的时候,很明显,我的网络请求的Cookie是空的!
App进程持久化Cookie
App进程应该如何修改Session Cookie呢?我想这个问题是很简单的:
1、通过HttpCookieStorage来写入的
2、通过网络请求Response Header中通过"Set-Cookie"写入的
那么将App进程的Cookie持久化的方案呢?一共有如下几种:
1、服务器设置Set Cookie
主要是需要设置expires过期时间,确保时间超过当前时间,同时需要设置sessionOnly属性为FALSE
设置的格式如下:
Set-cookie:name=name;expires=date;path=path;domain=domain;secure
实例如下:
Set-Cookie: age=11; Expires=Wed, 19 Oct 2022 07:28:00 GMT
Mac客户端收到该请求的response之后,就会将这个Cookie持久化为文件:Cookies.binarycookies,保存在本地,下次请求的时候,HttpCookieStorage.shared.cookies就是保存在本地的Cookie。
要是iOS客户端或者iPad客户端,这个文件是bundleID.binarycookies,这一点上iOS和OS X上略有不同。
该文件可以通过BinaryCookieReader.py来读取该cookie文件。
- cd在
BinaryCookieReader.py的文件目录下 - 执行
python BinaryCookieReader.py /Users/ryan/Library/Containers/com.ryan.macWebviewDemo.MacWebView/Data/Library/Cookies/Cookies.binarycookies
- 解析结果就会出现在命令行中
2、App端手动存储Cookie
直接通过归档的方式来保存Cookie到本地,然后在启动的时候加载持久化后的Cookie。
func saveCookie() {
let data = try? NSKeyedArchiver.archivedData(withRootObject: HTTPCookieStorage.shared.cookies!, requiringSecureCoding: true)
UserDefaults.standard.setValue(data, forKey: "come.test.cookie")
UserDefaults.standard.synchronize()
}
func loadCookie() -> [HTTPCookie]? {
guard let data = UserDefaults.standard.object(forKey: "come.test.cookie") as? Data else {
return nil
}
guard let cookies = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [HTTPCookie.self], from: data) as? [HTTPCookie] else {
return nil
}
return cookies
}
3、直接修改HTTPCookieStorage
网上的很多文章都说直接修改HTTPCooieStorage存储是不生效的,但是那些文章都有问题,经过我的测试,直接修改HTTPCooieStorage持久化Cookie是生效的。
这是设置Cookie的时候,一定要注意:
- expires 一定要设置一个超出当前时间的过期时间
- discard 这个字段千万不要设置,不知道为什么,不管设置成什么值,都会导致这个cookie变成session only
let property: [HTTPCookiePropertyKey: Any] = [.name: "age",
.value: "hui",
.domain: "mock.uutool.cn",
.path: "/",
.expires: Date.init(timeIntervalSinceNow: 3600)]
let httpCookie = HTTPCookie.init(properties: property)!
HTTPCookieStorage.shared.setCookie(httpCookie)
通过这种设置,无论是Mac端,iPhone端,还是iOS端都是可以持久化Cookie的!即以上设置完之后,cookie就会持久化为文件保存在沙盒里。
WKWebView持久化Cookie
对于iOS 11之后的系统WKWebView使用的是WKHTTPCookieStore来管理Cookie。根据以上的总结,我们可以得知在iOS上,WKWebView的cookie持久化之后是会保存在Cookies.binarycookies 文件中的,可是WKWebView的Cookie什么是时候持久化呢?
1、直接手动设置WKWebsiteDataStore
let property: [HTTPCookiePropertyKey: Any] = [.name: "age",
.value: "hui",
.domain: "mock.uutool.cn",
.path: "/",
.expires: Date.init(timeIntervalSinceNow: 3600)]
let httpCookie = HTTPCookie.init(properties: property)
let dataStorage = WKWebsiteDataStore.default()
dataStorage.httpCookieStore.setCookie(httpCookie!, completionHandler: nil)
let config = WKWebViewConfiguration.init()
config.websiteDataStore = dataStorage
// 开始请求
let url = URL.init(string: "https://mock.uutool.cn/hui")!
webView = WKWebView.init(frame: CGRect.init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height), configuration: config)
webView.navigationDelegate = self
webView.load(URLRequest.init(url: url))
它会在请求发出的时候就将Cookie持久化存储在本地文件中,经过我的测试,断点在decidePolicyFor代理方法,本地的Cookies.binarycookies文件就已经生成了。
当然啦,我也可以设置cookie不持久化到本地:
let dataStorage = WKWebsiteDataStore.nonPersistent()
2、服务器设置Set-Cookie
这一点也是WKWebView也上述一致,如果收到请求之后,服务器也设置了Set-Cookie的话,那么返回的cookie就会直接持久化到本地,那么,如果我不想持久化返回的这个Cookie呢?
和上述是一致的,使用nonPersistent方法提前设置即可。
let dataStorage = WKWebsiteDataStore.nonPersistent()
let config = WKWebViewConfiguration.init()
config.websiteDataStore = dataStorage
3、JS通过document.cookie来写入
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
NSLog("didFinish")
webView.evaluateJavaScript("document.cookie='age=18;expires=Wed, 19 Oct 2022 07:28:00 GMT'", completionHandler: nil)
}
直接通过webView来执行JS代码也可以使得cookie持久化。
App进程的Cookie和WebProcess进程的Cookie如何同步?
其实这个问题可以抽象为两个问题
问题1:WebProcess的Cookie如何同步到App进程呢?
因为在iOS 11之后,WKWebView统一使用WKHTTPCookieStore来管理Cookie, 而WKHTTPCookieStore提供了WKHTTPCookieStoreObserver来监听Cookie的变化。
这样的话,我们就可以在监听到WKWebViewCookie发生变化的时候,将Cookie同步到App进程。
func setupWebView() {
...
webView.configuration.websiteDataStore.httpCookieStore.add(self)
}
// WKHTTPCookieStoreObserver协议的方法
func cookiesDidChange(in cookieStore: WKHTTPCookieStore) {
cookieStore.getAllCookies { array in
array.forEach {
HTTPCookieStorage.shared.setCookie($0)
}
}
}
值得注意的是,这样的话,两种不同进程的Cookie都会持久化到本地。所以如果要HTTPCookieStorage的Cookie不持久化到本地的话,可以添加“discard”字段在HTTPCookie中,这也就是相当于设置“sessionOnly”属性了。
问题2:App进程的Cookie如何同步到WebProcess呢?
这个时候假设一个场景:App客户端请求了一个URL,服务器收到之后使用了Set-Cookies,当App客户端收到服务器的Response之后,需要把Cookies同步到WKWebView,那么如何实现呢?
可以采用淘宝技术分享中提到的解决方案:
我们可以通过 HOOK NSHTTPCookieStorage 中读写 Cookie 的接口,并且监听网络请求中 "Set-Cookie" 关键字,在 App 进程 Cookie 发生变化时同步到 WKWebView。
WKWebView可能会遇到的问题
坑点一:无法在WKWebView加载的Request里添加HTTP Body
这个已知的Bug是WebKit的已经存在的,现在已经修复了,所以现在是可以再Request里添加HttpBody的,但是添加HttpBody的时候,一定要注意指定设置HTTPHeaderField中的“Content-Type”:
var request = URLRequest.init(url: url)
request.httpMethod = "POST"
request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpBody = "do some thing".data(using: .utf8)
webView.load(request)
但是经过我的测试,在request设置为post请求的时候,使用request.addValue方法来添加Http头的时候,只有设置Content-Type是管用的,设置Accept,User-Agent等等都是不生效的, 不过有可能会有设置User-Agent的需求,这个可以通过WebView来直接设置:
webView.customUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36"
坑点二:无法在WKWebView加载的Request里设置HTTP Header
这个和上述问题一样,WKWebView的部分请求头是无法修改的。
具体怎么改,我也不知道~
参考资料
[1] 官方文档 trac.webkit.org/wiki/WebKit…
[2] 维基百科 en.wikipedia.org/wiki/WebKit
[3] 淘宝技术团队 zhuanlan.zhihu.com/p/393287432
[4] WKWebView那些坑 juejin.cn/post/684490…
[5] 干货:探秘WKWebView mp.weixin.qq.com/s/l9D4V0ON3…