背景
正在維護的App使用cookie來維護用戶登錄狀態(tài)嗅钻、App版本系統(tǒng)晾腔、語言等狀態(tài)信息。
在UIWebView時代啊犬,可以通過NSHTTPCookieStorage
單例很直接的管理客戶端cookie灼擂。UIWebView的cookie數(shù)據(jù)會自動和NSHTTPCookieStorage
進行同步。然而WKWebView的cookie維護一直為人詬觉至。只要你維護過相關(guān)業(yè)務(wù)剔应,不同iOS版本上出現(xiàn)的各種cookie的問題一定讓你頭疼過。
這個Demo是目前項目中使用的cookie管理方案语御,方案來回折騰了好幾個月峻贮,雖然不是很完整,但是基本滿足當(dāng)前項目需求应闯。
方案
初始化時WKWebsiteDataStore
使用[WKWebsiteDataStore defaultDataStore]
纤控,WKProcessPool
使用全局單例。
// webView初始化
- (WKWebView *)wkWebView{
if (!_wkWebView) {
WKWebViewConfiguration * config = [[WKWebViewConfiguration alloc]init];
config.allowsInlineMediaPlayback = YES;
config.selectionGranularity = YES;
config.processPool = [WebViewCookieUtil sharedProcessPool];
config.userContentController = self.userContentController;
_wkWebView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
_wkWebView.translatesAutoresizingMaskIntoConstraints = NO;
_wkWebView.navigationDelegate = self;
_wkWebView.UIDelegate = self;
[_wkWebView addObserver:self forKeyPath:NSStringFromSelector(@selector(estimatedProgress)) options:0 context:nil];
_wkWebView.opaque = NO;
if (@available(iOS 11.0, *)) {
_wkWebView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
}
return _wkWebView;
}
通過NSHTTPCookieStorage
維護客戶端cookie碉纺,當(dāng)cookie更新時首先更新NSHTTPCookieStorage
中船万,然后在不同的iOS版本使用不同的方式同步到WKWebView
中。
+ (void)clientCookieDidUpdate:(NSDictionary *)cookieDict toRemove:(NSArray *)toRemove {
// 客戶端cookie更新時調(diào)用骨田,比如用戶登錄狀態(tài)改變
NSHTTPCookieStorage *httpCookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
// 移除Cookie
NSArray<NSHTTPCookie *> *oldCookies = httpCookieStorage.cookies;
[oldCookies enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
for (NSString *name in toRemove) {
if ([obj.domain isEqualToString:name] && [obj.domain isEqualToString:domainForThisApp]) {
[httpCookieStorage deleteCookie:obj];
}
}
}];
// Write new Cookie to storage.
NSArray *cookieObjectArr = [self cookieObjectsFromCookieDict:cookieDict];
for (NSHTTPCookie *cookie in cookieObjectArr) {
[httpCookieStorage setCookie:cookie];
}
if (@available(iOS 11.0, *)) {
WKHTTPCookieStore *wkCookieStore = [WKWebsiteDataStore defaultDataStore].httpCookieStore;
[wkCookieStore getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull currentCookies) {
dispatch_group_t cleanGroup = dispatch_group_create();
for (NSHTTPCookie *cookie in currentCookies) {
for (NSString *name in toRemove) {
if ([cookie.domain isEqualToString:domainForThisApp] && [cookie.name isEqualToString:name]) {
dispatch_group_enter(cleanGroup);
[wkCookieStore deleteCookie:cookie completionHandler:^{
dispatch_group_leave(cleanGroup);
}];
}
}
}
dispatch_group_notify(cleanGroup, dispatch_get_main_queue(), ^{
for (NSHTTPCookie *cookie in cookieObjectArr) {
[wkCookieStore setCookie:cookie completionHandler:nil];
}
});
}];
} else {
// adjust userScript and reload webView if needed
}
}
iOS11以上
管理方式如上段代碼所示耿导,在cookie更新之后直接對[WKWebsiteDataStore defaultDataStore]
進行cookie設(shè)置即可。
潛在問題:
Cookie更新緩慢或無法更新态贤。
當(dāng)存在非視圖結(jié)構(gòu)中的WKWebView同時加載網(wǎng)頁時舱呻,WKHttpCookieStorage
的異步API回調(diào)可能會被阻塞導(dǎo)致cookie無法及時更新或者完全無法更新。
目前觀察該問題出現(xiàn)在iOS11.3~iOS12.2的系統(tǒng)悠汽。如果遇到相同問題可以首先排查是否有多個webView同時加載。
如果解問題難以發(fā)現(xiàn)或者解決成本較高,可以犧牲性能使用nonPersistentDataStore
來暫時規(guī)避這個問題
iOS10方案
iOS10或以下系統(tǒng)還沒有提供httpCookieStorage
茬射,我們需要使用WKUserScript
注入JS代碼的方式進行cookie更新吆倦。
// 創(chuàng)建userContentController時添加UserScript
- (WKUserContentController *)userContentController {
if (!_userContentController) {
WKUserContentController *userContentController = [WKUserContentController new];
if (@available(iOS 11.0, *)) {
[userContentController addUserScript:[WebViewCookieUtil cookieScriptForIOS10AndEarlier]];
}
_userContentController = userContentController;
}
return _userContentController;
}
將NSHTTPCookieStorage
中的cookie轉(zhuǎn)換成JS設(shè)置語句
// WKCookieUtil
+ (WKUserScript *)cookieScriptForIOS10AndEarlier {
NSMutableString *temp = @"".mutableCopy;
NSHTTPCookieStorage *httpCookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
[httpCookieStorage.cookies enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (![obj.domain isEqualToString:domainForThisApp]) {
return ;
}
NSString *foo = [NSString stringWithFormat:@"%@=%@;domain=%@;path=/",obj.name, obj.value, domainForThisApp];
[temp appendFormat:@"document.cookie = '%@';\n", foo];
}];
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource:[temp copy] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
return cookieScript;
}
即使已經(jīng)添加WKUserScript
,首個請求仍然需要通過設(shè)置request的httpHeader來帶上cookie信息。
- (void)loadURLWithCachePolicy:(NSURLRequestCachePolicy)cachePolicy {
// ...
if (@available(iOS 11.0, *)) {
// ...
} else {
NSString *cookieStr = [WebViewCookieUtil cookieStringForFirstRequestIOS10AndEarlier];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:self.URL cachePolicy:cachePolicy timeoutInterval:10];
[request addValue:cookieStr forHTTPHeaderField:@"Cookie"];
[self.wkWebView loadRequest:request];
}
}
// WKCookieUtil
+ (NSString *)cookieStringForFirstRequestIOS10AndEarlier {
NSHTTPCookieStorage *httpCookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
return [self cookieStringForCurrentDomain:httpCookieStorage.cookies];
}
+ (NSString *)cookieStringForCurrentDomain:(NSArray<NSHTTPCookie *> *)cookies {
NSMutableString *temp = @"".mutableCopy;
[temp appendFormat:@"domain=%@; path=%@; ", domainForThisApp, @"/"];
[cookies enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (![obj.domain isEqualToString:domainForThisApp]) {
return ;
}
[temp appendFormat:@"%@=%@; ", obj.name, obj.value];
}];
return [temp copy];
}
補充:
如果當(dāng)前webView使用過程中需要更新cookie,必須刪除之前的WKUserScript
,重新添加新的WKUserScript
爵憎,然后reload整個頁面。