WKWebView概述
-
WKWebView是蘋果在WWDC 2014 上推出的新一代WebView組件璃弄,相比iOS8及以前的UIWebView擁有更明顯的優(yōu)勢
- 更多的支持HTML5的特性
- 高達(dá)60fps的滾動刷新率以及內(nèi)置手勢
- 將UIWebViewDelegate與UIWebView拆分成了14類和3個協(xié)議
- Safari相同的JS引擎
- 占用更少的內(nèi)存
首先簡單熟悉一下WKWebView的屬性方法
// webview 配置
@property (nonatomic, readonly, copy) WKWebViewConfiguration *configuration;
//配置初始化方法
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
//WKPreferences偏好設(shè)置
config.preferences = [[WKPreferences alloc] init];
// 默認(rèn)為0
config.preferences.minimumFontSize = 10;
// 默認(rèn)認(rèn)為YES
config.preferences.javaScriptEnabled = YES;
// 在iOS上默認(rèn)為NO袁稽,表示不能自動通過窗口打開
config.preferences.javaScriptCanOpenWindowsAutomatically = NO;
// 導(dǎo)航代理
@property (nullable, nonatomic, weak) id <WKNavigationDelegate>navigationDelegate;
// 用戶交互代理
@property (nullable, nonatomic, weak) id <WKUIDelegate> UIDelegate;
// 與UIWebView一樣的加載請求API
- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
// 直接加載HTML
- (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
// 直接加載data
- (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString*)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL NS_AVAILABLE(10_11, 9_0);
// 停止加載數(shù)據(jù)
- (void)stopLoading;
// 執(zhí)行JS代碼
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler;
- JS和WebView內(nèi)容交互
// 只讀屬性蛛芥,所有添加的WKUserScript都在這里可以獲取到
@property (nonatomic, readonly, copy) NSArray<WKUserScript *> *userScripts;
// 注入JS
- (void)addUserScript:(WKUserScript *)userScript;
// 移除所有注入的JS
- (void)removeAllUserScripts;
// 添加scriptMessageHandler到所有的frames中锚赤,則都可以通過
// window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
// 發(fā)送消息
// JS要調(diào)用原生的方法的方式
- (void)addScriptMessageHandler:(id<WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
// 根據(jù)name移除所注入的scriptMessageHandler
- (void)removeScriptMessageHandlerForName:(NSString *)name;
- WKUserScript
在WKUserContentController中,所有使用到WKUserScript。WKUserContentController是用于與JS交互的類,而所注入的JS是WKUserScript對象偏陪。它的所有屬性和方法如下:
// JS源代碼
@property (nonatomic, readonly, copy) NSString *source;
// JS注入時間
@property (nonatomic, readonly) WKUserScriptInjectionTime injectionTime;
// 只讀屬性,表示JS是否應(yīng)該注入到所有的frames中還是只有main frame.
@property (nonatomic, readonly, getter=isForMainFrameOnly) BOOLforMainFrameOnly;
// 初始化方法煮嫌,用于創(chuàng)建WKUserScript對象
// source:JS源代碼
// injectionTime:JS注入的時間
// forMainFrameOnly:是否只注入main frame
- (instancetype)initWithSource:(NSString *)source injectionTime:(WKUserScriptInjectionTime)injectionTime forMainFrameOnly:(BOOL)forMainFrameOnly;
- WKNavigationDelegate
@protocol WKNavigationDelegate <NSObject>
@optional
// 決定導(dǎo)航的動作,通常用于處理跨域的鏈接能否導(dǎo)航抱虐。WebKit對跨域進(jìn)行了安全檢查限制昌阿,不允許跨域,因此我們要對不能跨域的鏈接
// 單獨處理。但是懦冰,對于Safari是允許跨域的灶轰,不用這么處理。
// 這個是決定是否Request
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
// 決定是否接收響應(yīng)
// 這個是決定是否接收response
// 要獲取response刷钢,通過WKNavigationResponse對象獲取
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
// 當(dāng)main frame的導(dǎo)航開始請求時笋颤,會調(diào)用此方法
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
// 當(dāng)main frame接收到服務(wù)重定向時,會回調(diào)此方法
- (void)webView:(WKWebView *)webViewdidReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation*)navigation;
// 當(dāng)main frame開始加載數(shù)據(jù)失敗時内地,會回調(diào)
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
// 當(dāng)main frame的web內(nèi)容開始到達(dá)時伴澄,會回調(diào)
- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation;
// 當(dāng)main frame導(dǎo)航完成時,會回調(diào)
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
// 當(dāng)main frame最后下載數(shù)據(jù)失敗時阱缓,會回調(diào)
- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
// 這與用于授權(quán)驗證的API非凌,與AFN、UIWebView的授權(quán)驗證API是一樣的
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *__nullable credential))completionHandler;
// 當(dāng)web content處理完成時荆针,會回調(diào)
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0);
@end
typedef NS_ENUM(NSInteger, WKNavigationActionPolicy) {
WKNavigationActionPolicyCancel,
WKNavigationActionPolicyAllow,
} NS_ENUM_AVAILABLE(10_10, 8_0);
WKWebView 和 UIWebView 的 Cookie 同步問題
- 由于 WKWebView 是 iOS9 之后推出的 webview 組件敞嗡,在這之前都是 UIWebView 。所以為了不影響 iOS8 上的 UIWebView 的使用航背,就要對 WKWebView 和 UIWebView 做封裝兼容處理喉悴。其中 Cookie 的同步問題是其中的一個大坑。
WKWebView 的 Cookie 機(jī)制:
WKWebView 的 Cookie 存儲在它的私有存儲 WKWebsiteDataStore 中玖媚。 WKWebsiteDataStore 中存儲了包括 cookies箕肃、disk、memory caches最盅、WebSQL突雪、IndexedDB 數(shù)據(jù)庫和本地存儲等Web內(nèi)容。
為什么說 WKWebsiteDataStore 是私有存儲容器呢 打開 WKWebsiteDataStore 的頭文件我們可以看到:
//defaultDataStore 是默認(rèn)選擇的存儲容器
+ (WKWebsiteDataStore *)defaultDataStore;
//nonPersistentDataStore會禁止任何數(shù)據(jù)寫入文件系統(tǒng)涡贱,可用于無痕瀏覽
+ (WKWebsiteDataStore *)nonPersistentDataStore;
//可以查看到容器中存儲的網(wǎng)站數(shù)據(jù)的所有種類
+ (NSSet<NSString *> *)allWebsiteDataTypes;
//以下分別是獲取容器中的數(shù)據(jù)記錄和刪除數(shù)據(jù)記錄的方法
- (void)fetchDataRecordsOfTypes:(NSSet<NSString *> *)dataTypes completionHandler:(void (^)(NSArray<WKWebsiteDataRecord *> *))completionHandler;
- (void)removeDataOfTypes:(NSSet<NSString *> *)dataTypes forDataRecords:(NSArray<WKWebsiteDataRecord *> *)dataRecords completionHandler:(void (^)(void))completionHandler;
- (void)removeDataOfTypes:(NSSet<NSString *> *)websiteDataTypes modifiedSince:(NSDate *)date completionHandler:(void (^)(void))completionHandler;
WKWebsiteDataStore 中的Web數(shù)據(jù)是以 WKWebsiteDataRecord 類的形式保存的咏删,我們可以通過 fetchDataRecordsOfTypes 方法獲取到 datastore 中的 record 數(shù)據(jù)。但是當(dāng)我們打開 WKWebsiteDataRecord 類的頭文件的時候就會發(fā)現(xiàn):
@interface WKWebsiteDataRecord : NSObject
/*! @abstract The display name for the data record. This is usually the domain name. */
@property (nonatomic, readonly, copy) NSString *displayName;
/*! @abstract The various types of website data that exist for this data record. */
@property (nonatomic, readonly, copy) NSSet<NSString *> *dataTypes;
@end
- 它一共就兩個公有屬性:displayName 和 dataTypes问词,分別是該 record 數(shù)據(jù)的域名名稱 和 該 record 中保存了哪些類型web數(shù)據(jù)督函。我們可以動手看看這里面到底是什么東西。
WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore];
[dataStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] completionHandler:^(NSArray<WKWebsiteDataRecord *> * _Nonnull records) {
for (WKWebsiteDataRecord *record in records)
{
NSLog(@"++++++++++++++++%@",[record description]);
}
}];
- 結(jié)果輸出如下:
- <WKWebsiteDataRecord: 0x1700e8a00; displayName = meitu.com; dataTypes = { Memory Cache, Cookies }>
- 所以在 WKWebsiteDataRecord 中我們是拿不到任何cookie數(shù)據(jù)的激挪。
UIWebView 的 Cookie 機(jī)制
UIWebView 在瀏覽網(wǎng)頁后會將網(wǎng)頁中的 cookie 自動存入 NSHTTPCookieStorage 標(biāo)準(zhǔn)容器中辰狡。在后續(xù)訪問中會將 cookie 自動帶到 request 請求當(dāng)中。比如垄分,NSHTTPCookieStorage 中存儲了一個Cookie宛篇,name=a;value=b;domain=y.qq.com;expires=Sat,02 May 2017 23:20:25 GMT; 則通 過 UIWebView 發(fā)起請求 http://y.qq.com,則請求頭會自動帶上cookie薄湿,而通過 WKWebView 發(fā)起請求叫倍,請求頭不會自動帶上該cookie偷卧。
初步解決方案
其實主要要做的只有兩步,1吆倦、獲取Cookie听诸,2、注入Cookie
1蚕泽、獲取Cookie
由于 WKWebView 的 Cookie 存儲容器 WKWebsiteDataStore 是私有存儲晌梨,所以無法從這里獲取到Cookie,目前的方法是(1)從網(wǎng)站返回的 response headerfields 中獲取须妻。(2)通過調(diào)用js的方法獲取 cookie仔蝌。
(1)從網(wǎng)站返回的 response headerfields 中獲取
因為cookie都存在http respone的headerfields,找到能獲得respone的WKWebView回調(diào),打印
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
NSArray *cookies =[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
//讀取wkwebview中的cookie 方法1
for (NSHTTPCookie *cookie in cookies) {
// [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
NSLog(@"wkwebview中的cookie:%@", cookie);
}
//讀取wkwebview中的cookie 方法2 讀取Set-Cookie字段
NSString *cookieString = [[response allHeaderFields] valueForKey:@"Set-Cookie"];
NSLog(@"wkwebview中的cookie:%@", cookieString);
//看看存入到了NSHTTPCookieStorage了沒有
NSHTTPCookieStorage *cookieJar2 = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in cookieJar2.cookies) {
NSLog(@"NSHTTPCookieStorage中的cookie%@", cookie);
}
decisionHandler(WKNavigationResponsePolicyAllow);
}
- (2)通過調(diào)用js的方法獲取 cookie。
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation
{
[webView evaluateJavaScript:[NSString stringWithFormat:@"document.cookie"] completionHandler:^(id _Nullable response, NSError * _Nullable error) {
if (response != 0) {
NSLog(@"\n\n\n\n\n\n document.cookie%@,%@",response,error);
}
}];
}
獲取 Cookie 的過程中碰到的坑
1璧南、通過(2)中 document.cookie 的方法獲取 cookie 有一定的可行性掌逛,但是實踐之后發(fā)現(xiàn)獲取到的cookie 并不全面,該方法無法獲取到 httponly 的cookie司倚。如果用 https://www.zhihu.com 試驗一下就會發(fā)現(xiàn)豆混,其中 z_c0 的 cookie 是無法被獲取到的,因為它是 httponly 屬性的cookie动知。
2皿伺、不論是(1)還是(2)方法,似乎都無法解決302請求的 Cookie 問題盒粮。舉例來說鸵鸥,假設(shè)你要訪問網(wǎng)站A,在A中點擊登錄丹皱,跳轉(zhuǎn)頁面到B地址妒穴,在B中完成登錄之后302跳轉(zhuǎn)回A網(wǎng)站。此時cookie是存在于B地址的 response 中的摊崭,在A地址的 response 中并沒有 cookie 的字段讼油。然而我們只能獲取到A地址的 response ,無法截獲到B地址的response呢簸。因此獲取不到該類型網(wǎng)站的 cookie 矮台。(該問題還在嘗試解決中,如果有解決方案的小伙伴希望能告知我一聲感激不盡)
2根时、注入Cookie
注入 Cookie 就是從之前保存 cookie 的 NSHTTPCookieStorage 中取出相關(guān) Cookie,然后在再次請求訪問的時候在 request 中注入 Cookie瘦赫。注入Cookie同樣有多種方式。
(1)JS注入1
//取出 storage 中的cookie并將其拼接成正確的形式
NSArray<NSHTTPCookie *> *tmp = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
NSMutableString *jscode_Cookie = [@"" mutableCopy];
[tmp enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"%@ = %@", obj.name, obj.value);
[jscode_Cookie appendString:[NSString stringWithFormat:@"document.cookie = '%@=%@';", obj.name, obj.value]];
}];
WKUserContentController* userContentController = WKUserContentController.new;
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:cookieScript];
WKWebViewConfiguration* webViewConfig = WKWebViewConfiguration.new;
webViewConfig.userContentController = userContentController;
WKWebView * webView = [[WKWebView alloc] initWithFrame:CGRectMake(/*set your values*/) configuration:webViewConfig];
- (2)JS注入2
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
[webView evaluateJavaScript:@"document.cookie ='TeskCookieKey1=TeskCookieValue1';" completionHandler:^(id result, NSError *error) {
//...
}];
}
- (3) NSMutableURLRequest 注入Cookie
NSURL *url = request.URL;
NSMutableString *cookies = [NSMutableString string];
NSMutableURLRequest *requestObj = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10];
NSArray *tmp = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
NSDictionary *dicCookies = [NSHTTPCookie requestHeaderFieldsWithCookies:tmp];
NSString *cookie = [self readCurrentCookie];
[requestObj setValue:cookie forHTTPHeaderField:@"Cookie"];
[_webView loadRequest:requestObj];
-(NSString *)readCurrentCookie{
NSMutableDictionary *cookieDic = [NSMutableDictionary dictionary];
NSMutableString *cookieValue = [NSMutableString stringWithFormat:@""];
NSHTTPCookieStorage *cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [cookieJar cookies]) {
[cookieDic setObject:cookie.value forKey:cookie.name];
}
// cookie重復(fù)蛤迎,先放到字典進(jìn)行去重确虱,再進(jìn)行拼接
for (NSString *key in cookieDic) {
NSString *appendString = [NSString stringWithFormat:@"%@=%@;", key, [cookieDic valueForKey:key]];
[cookieValue appendString:appendString];
}
return cookieValue;
}
- 注入 Cookie 的過程中碰到的坑
(1)JS注入的Cookie,比如PHP代碼在Cookie容器中取是取不到的,直接js document.cookie能讀取到
NSMutableURLRequest 注入的PHP等動態(tài)語言直接能從$_COOKIE對象中獲取到
(2)app退出后 cookie 丟失的問題
- 在開發(fā)過程中發(fā)現(xiàn) cookie 的保存上出現(xiàn)了問題,在注入 cookie 的時候發(fā)現(xiàn)部分 Cookie 丟失替裆。網(wǎng)上找到另外一種解決方案是說 cookie 過期蝉娜,需要手動設(shè)置保存一下時間唱较。默認(rèn)下扎唾,cookies 的保存時間是 app 退出后 cookie 就會被清掉召川。可參考:http://www.reibang.com/p/1e402922ee32/ 胸遇。雖然我在設(shè)置了 cookie 的有效期之后荧呐,還是存在 cookie 丟失的問題。(嘆氣
- 于是后來采用 NSUserDefault 在獲取到 cookie 的時候保存 cookie纸镊,在重新訪問的時候從 NSUserDefault 中取出 cookie 再設(shè)置一遍倍阐,從而得到之前保存的 cookie 并注入request 中
//修改保存 cookie 到storage的方法
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(nonnull WKNavigationResponse *)navigationResponse decisionHandler:(nonnull void (^)(WKNavigationResponsePolicy))decisionHandler{
NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
NSArray *cookies =[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
NSString *cookieString = [[response allHeaderFields] valueForKey:@"Set-Cookie"];
for (NSHTTPCookie *cookie in cookies) {
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
}
NSMutableArray *cookieArray = [[NSMutableArray alloc] init];
for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
[cookieArray addObject:cookie.name];
NSMutableDictionary *cookieProperties = [NSMutableDictionary dictionary];
[cookieProperties setObject:cookie.name forKey:NSHTTPCookieName];
[cookieProperties setObject:cookie.value forKey:NSHTTPCookieValue];
[cookieProperties setObject:cookie.domain forKey:NSHTTPCookieDomain];
[cookieProperties setObject:cookie.path forKey:NSHTTPCookiePath];
[cookieProperties setObject:[NSNumber numberWithInt:cookie.version] forKey:NSHTTPCookieVersion];
[cookieProperties setObject:[[NSDate date] dateByAddingTimeInterval:2629743] forKey:NSHTTPCookieExpires];
[[NSUserDefaults standardUserDefaults] setValue:cookieProperties forKey:cookie.name];
[[NSUserDefaults standardUserDefaults] synchronize];
}
[[NSUserDefaults standardUserDefaults] setValue:cookieArray forKey:@"cookieArray"];
[[NSUserDefaults standardUserDefaults] synchronize];
//清除WKWebView的Cookie,之后刪除
WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore];
[dataStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes]
completionHandler:^(NSArray<WKWebsiteDataRecord *> * __nonnull records) {
for (WKWebsiteDataRecord *record in records)
{
// if ( [record.displayName containsString:@"baidu"]) //取消備注逗威,可以針對某域名清除峰搪,否則是全清
// {
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:record.dataTypes
forDataRecords:@[record]
completionHandler:^{
NSLog(@"Cookies for %@ deleted successfully",record.displayName);
}];
// }
}
}];
decisionHandler(WKNavigationResponsePolicyAllow);
}
//修改從 storage 中讀取 cookie 的方法
-(NSString *)readCurrentCookie{
NSMutableArray* cookieDictionary = [[NSUserDefaults standardUserDefaults] valueForKey:@"cookieArray"];
NSLog(@"cookie dictionary found is %@",cookieDictionary);
for (int i=0; i < cookieDictionary.count; i++)
{
NSLog(@"cookie found is %@",[cookieDictionary objectAtIndex:i]);
NSMutableDictionary* cookieDictionary1 = [[NSUserDefaults standardUserDefaults] valueForKey:[cookieDictionary objectAtIndex:i]];
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:cookieDictionary1];
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
}
NSMutableDictionary *cookieDic = [NSMutableDictionary dictionary];
NSMutableString *cookieValue = [NSMutableString stringWithFormat:@""];
NSHTTPCookieStorage *cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [cookieJar cookies]) {
[cookieDic setObject:cookie.value forKey:cookie.name];
}
// cookie重復(fù),先放到字典進(jìn)行去重凯旭,再進(jìn)行拼接
for (NSString *key in cookieDic) {
// if ([key isEqualToString:@"nweb_qa"] || [key isEqualToString:@"z_c0"] || [key isEqualToString:@"_xsrf"]) {
NSString *appendString = [NSString stringWithFormat:@"%@=%@;", key, [cookieDic valueForKey:key]];
[cookieValue appendString:appendString];
// }
}
return cookieValue;
}
結(jié)語
- WKWebView cookie 的同步問題中的坑還是挺多的概耻,而且目前還是沒有解決獲取重定向地址Cookie的問題,有相關(guān)建議或者困惑的同學(xué)可以評論或者私聊一起討論解決一下罐呼。