說起 WKWebview代替 UIWebview 帶來的好處你可以舉出一堆堆的例子,但說到 WKWebview 的問題,你繞不過的就是 WKWebview cookie和 NSHTTPCookieStorage cookie 不共享的問題。你可以在網(wǎng)絡上搜到如何將他們相互同步的帖子。

如何將 NSHTTPCookieStorage 同步給 WKWebview ,大概要處理很多種情況,包括但不限于以下;
- 初次加載頁面時,同步
cookie到WKWebview- 處理 ajax 請求時,需要的
cookie- 如果
response里有set-cookie還需要緩存這些cookie- 如果是
302還需要處理cookie傳遞的問題
所以,如果你按照上面的要求編寫了代碼,你會發(fā)現(xiàn)總有漏網(wǎng)之魚的情況沒有處理,比方說請求response 設置了cookie,為了在后續(xù)跳轉(zhuǎn)中帶上這些 cookie,你需要暫存下來,這樣可能會污染到 NSHTTPCookieStorage ;再舉一個極端的真實的案例,如果有個網(wǎng)站的鑒權(quán)是通過 302 鑒權(quán) 和 response set-cookie 的,那么你會發(fā)現(xiàn)這個網(wǎng)站在鑒權(quán)那里陷入了死循環(huán),因為 302 response set-cookie 后 302 的 location 地址加載時并沒有攜帶上 302 時設置的 cookie,進而繼續(xù) 302 set-cookie的跳轉(zhuǎn)。
那如果解決 302 response set-cookie 的問題,我們不能在上述方案里修修補補,上述方案對正常的數(shù)據(jù)請求已經(jīng)有很大的侵入性,對很多沒有必要進行 cookie 設置的頁面做了處理,一定程度上對性能也有影響。讓我們跳脫原來的方案,重新審視下 WKWebview cookie 相關(guān)的資料。
WKWebview cookie 是怎么存儲的
- session 級別的 cookie
session 級別的 cookie 是保存在 WKProcessPool 里的,每個 WKWebview 都可以關(guān)聯(lián)一個 WKProcessPool 的實例,如果需要在整個 App 生命周期里訪問 h5 保留 h5 里的登錄狀態(tài)的,可以將使用 WKProcessPool 的單例來共享登錄狀態(tài)。
WKProcessPool是個沒有屬性和方法的對象,唯一的作用就是標識是不是需要新的 session 級別的管理對象,一個實例代表一個對象。
- 未過期的 cookie
有有效期的 cookie被持久化存儲在 NSLibraryDirectory 目錄下的 Cookies/文件夾。
注意,
cookie持久化文件地址在iOS 9+上在/Users/Mac/Library/Developer/CoreSimulator/Devices/D2F74420-D59B-4A15-A50B-774D3D01FADE/data/Containers/Data/Application/E8646AD5-1110-43F3-95D9-DE6A32E78DB7/Library/Cookies.
但是在iOS 8 上 cookie被保存在兩部分,一部分如上所述,還有一部分保存在 App 無法獲取的地方,/Users/Mac/Library/Developer/CoreSimulator/Devices/D2F74420-D59B-4A15-A50B-774D3D01FADE/data/Library/Cookies,大概就是后者的Cookie是iOS 的 Safari使用 。
在Cookies 目錄下兩個文件比較重要;
Cookie.binarycookies<appid>.binarycookies
兩者的區(qū)別是 <appid>.binarycookies 是 NSHTTPCookieStorage 文件對象;<appid>.binarycookies 則是 WKWebview 的實例化對象。
這也是為什么 WKWebview 和 NSHTTPCookieStorage 的原因——因為被保存在不同的文件當中。
為了驗證,你可以打開這兩者文件進行查看,這里不再展開。
當然兩個文件都是
binary file,直接用文本瀏覽器打開是看不到,有一個python寫的腳本BinaryCookieReadergist.github.com/sh1n0b1/4bb…。可以讀出來
WKWebview Cookie 是如何工作的?
- 當
webview loadRequest或者302或者在webview加載完畢,觸發(fā)了 ajax 請求時,WKWebview所需的Cookie會去Cookie.binarycookies里讀取本域名下的Cookie,加上
WKProcessPool持有的Cookie以前作為request頭里的Cookie數(shù)據(jù)。
- 當
- 但是如果仔細查看
NSURLRequest.h源代碼,而不是僅僅查看NSDictionary<NSString *, NSString *> *allHTTPHeaderFields;的 quick help,你會發(fā)現(xiàn)這句話;
- 但是如果仔細查看
@abstract Sets the HTTP header fields of the receiver to the given
dictionary.
@discussion This method replaces all header fields that may have
existed before this method call.
再查看下HTTPShouldHandleCookies 的 quick help,
@property BOOL HTTPShouldHandleCookies;
Description
A boolean value that indicates whether the receiver should use the default cookie handling for the request.
YES if the receiver should use the default cookie handling for the request, NO otherwise. The default is YES.
If your app sets the Cookie header on an NSMutableURLRequest object, then this method has no effect, and the cookie data you set in the header overrides all cookies from the cookie store.
SDKs iOS 8.0+, macOS 10.10+, tvOS 9.0+, watchOS 2.0+
結(jié)合兩者,你也會發(fā)現(xiàn)一個核心的概念-如果設置了 allHTTPHeaderFields,則不用使用 the cookie manager by default。
所以我們的方案是-在頁面加載過程中不去設置allHTTPHeaderFields,全部使用默認 Cookie mananger 管理,這樣就不會有 Cookie 污染也不會有 302 Cookie 丟失的問題了,下面讓我們驗證一下。
唯一的問題——如何將 NSHTTPCookieStorage 的 Cookie 共享給 WKWebview。
解決方案
在首次加載 url 時,檢查是否已經(jīng)同步過 Cookie。如果沒有同步過,則先加載 一個 cookieWebivew,它的主要目的就是將 Cookie 先使用 usercontroller 的方式寫到 WKWebview 里,這樣在處理正式的請求時,就會帶上我們從NSHTTPCookieStorage 獲取到的 Cookie了。 核心代碼如下,
if ([AppHostCookie loginCookieHasBeenSynced] == NO) {
//
NSURL *cookieURL = [NSURL URLWithString:kFakeCookieWebPageURLString];
NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:cookieURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:120];
WKWebView *cookieWebview = [self getCookieWebview];
[self.view addSubview:cookieWebview];
[cookieWebview loadRequest:mutableRequest];
DDLogInfo(@"[JSBridge] preload cookie for url = %@", self.loadUrl);
} else {
[self loadWebPage];
}
// 注意,CookieWebview 和 正常的 webview 是不同的
- (WKWebView *)getCookieWebview
{
// 設置加載頁面完畢后,里面的后續(xù)請求,如 xhr 請求使用的cookie
WKUserContentController *userContentController = [WKUserContentController new];
WKWebViewConfiguration *webViewConfig = [[WKWebViewConfiguration alloc] init];
webViewConfig.userContentController = userContentController;
webViewConfig.processPool = [AppHostCookie sharedPoolManager];
NSMutableArray<NSString *> *oldCookies = [AppHostCookie cookieJavaScriptArray];
[oldCookies enumerateObjectsUsingBlock:^(NSString *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
NSString *setCookie = [NSString stringWithFormat:@"document.cookie='%@';", obj];
WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:setCookie injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
[userContentController addUserScript:cookieScript];
}];
WKWebView *webview = [[WKWebView alloc] initWithFrame:CGRectMake(0, -1, SCREEN_WIDTH,ONE_PIXEL) configuration:webViewConfig];
webview.navigationDelegate = self;
webview.UIDelegate = self;
return webview;
}
這里需要處理的問題是,加載完畢或者失敗后需要清理舊 webview 和設置標記位。
static NSString * _Nonnull kFakeCookieWebPageURLString = @"http://ai.api.com/xhr/user/getUid.do?26u-KQa-fKQ-3BD"
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
NSURL *targetURL = webView.URL;
if ([AppHostCookie loginCookieHasBeenSynced] == NO && targetURL.query.length > 0 && [kFakeCookieWebPageURLString containsString:targetURL.query]) {
[AppHostCookie setLoginCookieHasBeenSynced:YES];
// 加載真正的頁面;此時已經(jīng)有 App 的 cookie 存在了。
[webView removeFromSuperview];
[self loadWebPage];
return;
}
}
同時記得刪掉原來對 webview 的 Cookie 的所有處理的代碼。
處理至此,大功告成,這樣的后續(xù)請求, WKWebview 都用自身所有的Cookie和 NSHTTPCookieStorage 的 Cookie,這樣既達到了 Cookie 共享的目的, WKWebview 和 NSHTTPCookieStorage 的 Cookie 也做了個隔離。