WKWebview 處理

前言

關(guān)于UIWebView的介紹,相信看過(guò)上文的小伙伴們,已經(jīng)大概清楚了吧,如果有問(wèn)題,歡迎提問(wèn)。

本文是本系列文章的第二篇,主要為小伙伴們分享下WKWebView相關(guān)的內(nèi)容:

iOS中UIWebView與WKWebView、JavaScript與OC交互、Cookie管理看我就夠(上)

iOS中UIWebView與WKWebView、JavaScript與OC交互、Cookie管理看我就夠(中)

iOS中UIWebView與WKWebView、JavaScript與OC交互、Cookie管理看我就夠(下)(已發(fā)布??)

關(guān)于文中提到的一些內(nèi)容,這里我準(zhǔn)備了個(gè)Demo,有需要的小伙伴可以下載。

本文目錄

前言

WKWebView

簡(jiǎn)介

基本用法

創(chuàng)建

動(dòng)態(tài)注入js

加載

代理

新屬性

JavaScript與Objective-C的交互

OC -> JS

JS -> OC

URL攔截

scriptMessageHandler

實(shí)際運(yùn)用

Cookie管理

解決首次加載Cookie帶不上問(wèn)題

解決后續(xù)Ajax請(qǐng)求Cookie丟失問(wèn)題

解決跳轉(zhuǎn)新頁(yè)面時(shí)Cookie帶不過(guò)去問(wèn)題

解決上面3步都做了Cookie依然丟失

性能對(duì)比

各種坑

js alert方法不彈窗

白屏問(wèn)題

Cookie丟失

evaluateJavaScript:completionHandler:異步

自定義contentInset刷新時(shí)頁(yè)面跳動(dòng)的bug

加載POST請(qǐng)求丟失RequestBody

NSURLProtocol問(wèn)題

未完待續(xù)

WKWebView

簡(jiǎn)介

WKWebView是Apple于iOS 8.0推出的WebKit中的核心控件,用來(lái)替代UIWebView。WKWebView比UIWebView的優(yōu)勢(shì)在于:

更多的支持HTML5的特性

高達(dá)60fps的滾動(dòng)刷新率以及內(nèi)置手勢(shì)

與Safari相同的JavaScript引擎

將UIWebViewDelegate與UIWebView拆分成了14類與3個(gè)協(xié)議(官方文檔說(shuō)明

可以獲取加載進(jìn)度:estimatedProgress(UIWebView需要調(diào)用私有Api)

作者本人在項(xiàng)目中使用WKWebView也1年多了,確確實(shí)實(shí)感受到了它的優(yōu)勢(shì),但是同樣也感受到了它帶來(lái)的一些坑。下面來(lái)具體的介紹下WKWebView。其實(shí)Apple開(kāi)源了WebKit,有興趣的小伙伴可以研究下它的實(shí)現(xiàn)。

基本用法

創(chuàng)建

WKWebView的創(chuàng)建方法有這兩種

/*-initWithFrame: to initialize an instance with the default configuration. 如果使用initWithFrame方法將使用默認(rèn)的configuration

The initializer copies the specified configuration, so mutating the configuration after invoking the initializer has no effect on the web view. 我們需要先設(shè)置configuration,再調(diào)用init,在init之后修改configuration則無(wú)效

*/- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration*)configurationNS_DESIGNATED_INITIALIZER;- (nullableinstancetype)initWithCoder:(NSCoder*)coderNS_DESIGNATED_INITIALIZER;

仔細(xì)看第一個(gè)方法,比UIWebView多了個(gè)configuration,這個(gè)配置可以設(shè)置很多東西。具體查看WKWebViewConfiguration.h,可以配置js是否支持,畫中畫是否開(kāi)啟等,這里主要講兩個(gè)比較常用的屬性。

第一個(gè)屬性是websiteDataStore。

/*! @abstract The website data store to be used by the web view.

*/@property(nonatomic,strong)WKWebsiteDataStore*websiteDataStore API_AVAILABLE(macosx(10.11), ios(9.0));

業(yè)界普遍認(rèn)為WKWebView擁有自己的私有存儲(chǔ),它的一些緩存等數(shù)據(jù)都存在websiteDataStore中,具體增刪改查就可以通過(guò)WKWebsiteDataStore.h中提供的方法,這里不多說(shuō),一般用的時(shí)候比較少,真的要清除緩存,簡(jiǎn)單粗暴的方法是刪除沙盒目錄中的Cache文件夾。

第二個(gè)屬性是userContentController。

/*! @abstract Theusercontentcontroller to associate with the web view.*/@property(nonatomic, strong) WKUserContentController *userContentController;

這個(gè)屬性很重要,后面講的js->oc的交互,以及注入js代碼都會(huì)用到它。查看WKUserContentController的頭文件,你會(huì)發(fā)現(xiàn)它有如下幾個(gè)方法:

@interfaceWKUserContentController:NSObject//讀取添加過(guò)的腳本@property(nonatomic,readonly,copy)NSArray *userScripts;//添加腳本- (void)addUserScript:(WKUserScript*)userScript;//刪除所有添加的腳本- (void)removeAllUserScripts;//通過(guò)window.webkit.messageHandlers..postMessage() 來(lái)實(shí)現(xiàn)js->oc傳遞消息,并添加handler- (void)addScriptMessageHandler:(id)scriptMessageHandler name:(NSString*)name;//刪除handler- (void)removeScriptMessageHandlerForName:(NSString*)name;@end

那么整體我創(chuàng)建一個(gè)WKWebView的代碼如下:

WKWebViewConfiguration*configuration = [[WKWebViewConfigurationalloc] init];WKUserContentController*controller = [[WKUserContentControlleralloc] init];configuration.userContentController = controller;self.webView = [[WKWebViewalloc] initWithFrame:self.view.bounds configuration:configuration];self.webView.allowsBackForwardNavigationGestures =YES;//允許右滑返回上個(gè)鏈接,左滑前進(jìn)self.webView.allowsLinkPreview =YES;//允許鏈接3D Touchself.webView.customUserAgent =@"WebViewDemo/1.0.0";//自定義UA,UIWebView就沒(méi)有此功能,后面會(huì)講到通過(guò)其他方式實(shí)現(xiàn)self.webView.UIDelegate =self;self.webView.navigationDelegate =self;[self.view addSubview:self.webView];

動(dòng)態(tài)注入js

通過(guò)給userContentController添加WKUserScript,可以實(shí)現(xiàn)動(dòng)態(tài)注入js。比如我先注入一個(gè)腳本,給每個(gè)頁(yè)面添加一個(gè)Cookie

//注入一個(gè)CookieWKUserScript *newCookieScript = [[WKUserScript alloc]initWithSource:@"document.cookie = 'DarkAngelCookie=DarkAngel;'"injectionTime:WKUserScriptInjectionTimeAtDocumentStartforMainFrameOnly:NO];[controlleraddUserScript:newCookieScript];

然后再注入一個(gè)腳本,每當(dāng)頁(yè)面加載,就會(huì)alert當(dāng)前頁(yè)面cookie,在OC中的實(shí)現(xiàn)

//創(chuàng)建腳本W(wǎng)KUserScript *cookieScript = [[WKUserScript alloc]initWithSource:@"alert(document.cookie);"injectionTime:WKUserScriptInjectionTimeAtDocumentEndforMainFrameOnly:NO];//添加腳本[controlleraddUserScript:script];

這樣每當(dāng)頁(yè)面出現(xiàn)的時(shí)候,會(huì)alet彈出當(dāng)前頁(yè)面所有的cookie字符串。

注入的js source可以是任何js字符串,也可以js文件。比如你有很多提供給h5使用的js方法,那么你本地可能就會(huì)有一個(gè)native_functions.js,你可以通過(guò)以下的方式添加

//防止頻繁IO操作,造成性能影響staticNSString*jsSource;staticdispatch_once_tonceToken;dispatch_once(&onceToken, ^{? ? ? jsSource = [NSStringstringWithContentsOfFile:[[NSBundlemainBundle] pathForResource:@"native_functions"ofType:@"js"] encoding:NSUTF8StringEncodingerror:nil];});//添加自定義的腳本W(wǎng)KUserScript*js = [[WKUserScriptalloc] initWithSource:jsSource injectionTime:WKUserScriptInjectionTimeAtDocumentEndforMainFrameOnly:NO];[self.configuration.userContentController addUserScript:js];

加載

加載一個(gè)請(qǐng)求或者頁(yè)面也很簡(jiǎn)單

- (nullableWKNavigation*)loadRequest:(NSURLRequest*)request;- (nullableWKNavigation*)loadFileURL:(NSURL*)URL allowingReadAccessToURL:(NSURL*)readAccessURL API_AVAILABLE(macosx(10.11), ios(9.0));- (nullableWKNavigation*)loadHTMLString:(NSString*)string baseURL:(nullableNSURL*)baseURL;- (nullableWKNavigation*)loadData:(NSData*)data MIMEType:(NSString*)MIMEType characterEncodingName:(NSString*)characterEncodingName baseURL:(NSURL*)baseURL API_AVAILABLE(macosx(10.11), ios(9.0));

基本與UIWebView的很相似,但是需要說(shuō)明的是,加載本地的一個(gè)html需要使用loadRequest:方法,使用loadHTMLString:baseURL:方法會(huì)有問(wèn)題。

[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"test"ofType:@"html"]]]];

代理

在WKWebView的頭文件,你會(huì)發(fā)現(xiàn)

@protocolWKNavigationDelegate;//類似于UIWebView的加載成功、失敗、是否允許跳轉(zhuǎn)等@protocolWKUIDelegate;//主要是一些alert、打開(kāi)新窗口之類的

有兩個(gè)協(xié)議,它將UIWebView的代理協(xié)議拆成了一個(gè)跳轉(zhuǎn)的協(xié)議和一個(gè)關(guān)于UI的協(xié)議。雖說(shuō)這兩個(gè)協(xié)議中的所有方法都是Optional,但是關(guān)于WKUIDelegate協(xié)議是有坑的,后面的各種坑中會(huì)提到。簡(jiǎn)單說(shuō)下WKNavigationDelegate中比較常用的方法

//下面這2個(gè)方法共同對(duì)應(yīng)了UIWebView的 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;//先:針對(duì)一次action來(lái)決定是否允許跳轉(zhuǎn),action中可以獲取request,允許與否都需要調(diào)用decisionHandler,比如decisionHandler(WKNavigationActionPolicyCancel);- (void)webView:(WKWebView*)webView decidePolicyForNavigationAction:(WKNavigationAction*)navigationAction decisionHandler:(void(^)(WKNavigationActionPolicy))decisionHandler;//后:根據(jù)response來(lái)決定,是否允許跳轉(zhuǎn),允許與否都需要調(diào)用decisionHandler,如decisionHandler(WKNavigationResponsePolicyAllow);- (void)webView:(WKWebView*)webView decidePolicyForNavigationResponse:(WKNavigationResponse*)navigationResponse decisionHandler:(void(^)(WKNavigationResponsePolicy))decisionHandler;//開(kāi)始加載,對(duì)應(yīng)UIWebView的- (void)webViewDidStartLoad:(UIWebView *)webView;- (void)webView:(WKWebView*)webView didStartProvisionalNavigation:(null_unspecifiedWKNavigation*)navigation;//加載成功,對(duì)應(yīng)UIWebView的- (void)webViewDidFinishLoad:(UIWebView *)webView;- (void)webView:(WKWebView*)webView didFinishNavigation:(null_unspecifiedWKNavigation*)navigation;//加載失敗,對(duì)應(yīng)UIWebView的- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error;- (void)webView:(WKWebView*)webView didFailNavigation:(null_unspecifiedWKNavigation*)navigation withError:(NSError*)error;

WKUIDelegate這里先不提了,小伙伴們可以參考我Demo中的實(shí)現(xiàn)。

新屬性

WKWebView.h定義了如下幾個(gè)常用的readonly屬性:

@property(nullable,nonatomic,readonly,copy)NSString*title;//頁(yè)面的title,終于可以直接獲取了@property(nullable,nonatomic,readonly,copy)NSURL*URL;//當(dāng)前webView的URL@property(nonatomic,readonly,getter=isLoading)BOOLloading;//是否正在加載@property(nonatomic,readonly)doubleestimatedProgress;//加載的進(jìn)度@property(nonatomic,readonly)BOOLcanGoBack;//是否可以后退,跟UIWebView相同@property(nonatomic,readonly)BOOLcanGoForward;//是否可以前進(jìn),跟UIWebView相同

這些屬性都很有用,而且支持KVO,所以我們可以通過(guò)KVO觀察這些值的變化,以便于我們做出最友好的交互。

JavaScript與Objective-C的交互

介紹完WKWebView的基本用法,讓我們來(lái)研究下基于它的js與oc的交互。

OC -> JS

這個(gè)比較簡(jiǎn)單,WKWebView提供了一個(gè)類似JavaScriptCore的方法

//執(zhí)行一段js,并將結(jié)果返回,如果出錯(cuò),error則不為空-(void)evaluateJavaScript:(NSString*)javaScriptStringcompletionHandler:(void(^ _Nullable)(_Nullable id result, NSError * _Nullable error))completionHandler;

該方法很好的解決了之前文章中提到的UIWebView使用stringByEvaluatingJavaScriptFromString:方法的兩個(gè)缺點(diǎn)(1. 返回值只能是NSString。2. 報(bào)錯(cuò)無(wú)法捕獲)。比如我想獲取頁(yè)面中的title,除了直接self.webView.title外,還可以通過(guò)這個(gè)方法:

[self.webView evaluateJavaScript:@"document.title"completionHandler:^(id_Nullable title, NSError * _Nullable error) {? ? ? ? NSLog(@"調(diào)用evaluateJavaScript異步獲取title:%@", title);}];

JS -> OC

URL攔截

此方法與上篇文章中UIWebView介紹到的URL攔截方法一致,都是通過(guò)自定義Scheme,在鏈接激活時(shí),攔截該URL,拿到參數(shù),調(diào)用OC方法,缺點(diǎn)依然明顯。WKWebView實(shí)現(xiàn)起來(lái)如下:

比如我的鏈接依然是

短信驗(yàn)證登錄

當(dāng)用戶點(diǎn)擊這個(gè)a標(biāo)簽時(shí),會(huì)被攔截

- (void)webView:(WKWebView*)webView decidePolicyForNavigationAction:(WKNavigationAction*)navigationAction decisionHandler:(void(^)(WKNavigationActionPolicy))decisionHandler {//可以通過(guò)navigationAction.navigationType獲取跳轉(zhuǎn)類型,如新鏈接、后退等NSURL*URL = navigationAction.request.URL;//判斷URL是否符合自定義的URL Schemeif([URL.scheme isEqualToString:@"darkangel"]) {//根據(jù)不同的業(yè)務(wù),來(lái)執(zhí)行對(duì)應(yīng)的操作,且獲取參數(shù)if([URL.host isEqualToString:@"smsLogin"]) {NSString*param = URL.query;NSLog(@"短信驗(yàn)證碼登錄, 參數(shù)為%@", param);? ? ? ? ? ? decisionHandler(WKNavigationActionPolicyCancel);return;? ? ? ? }? ? }? ? decisionHandler(WKNavigationActionPolicyAllow);NSLog(@"%@",NSStringFromSelector(_cmd));}

整體實(shí)現(xiàn)是與UIWebView十分相似的,這里就不多說(shuō)了。

這里再次提一下WebViewJavascriptBridge,它在最近的新版本中支持了WKWebView。使用的方案同樣是攔截URL,具體原理在之前的文章中簡(jiǎn)單描述過(guò),這里不再贅述。下面說(shuō)下Apple的新方法。

scriptMessageHandler

這是Apple在WebKit里新增加的方法,位于WKUserContentController.h。

/*!@abstractAdds a script message handler.@paramscriptMessageHandler The message handler to add.@paramname The name of the message handler.@discussionAdding a scriptMessageHandler adds a function window.webkit.messageHandlers..postMessage() for all frames. */- (void)addScriptMessageHandler:(id )scriptMessageHandler name:(NSString *)name;/*!@abstractRemoves a script message handler.@paramname The name of the message handler to remove. */- (void)removeScriptMessageHandlerForName:(NSString *)name;

其實(shí)Apple的注釋已經(jīng)很清楚了,在OC中添加一個(gè)scriptMessageHandler,則會(huì)在all frames中添加一個(gè)js的function:window.webkit.messageHandlers..postMessage()。那么當(dāng)我在OC中通過(guò)如下的方法添加了一個(gè)handler,如

[controlleraddScriptMessageHandler:selfname:@"currentCookies"];//這里self要遵循協(xié) WKScriptMessageHandler

則當(dāng)我在js中調(diào)用下面的方法時(shí)

window.webkit.messageHandlers.currentCookies.postMessage(document.cookie);

我在OC中將會(huì)收到WKScriptMessageHandler的回調(diào)

- (void)userContentController:(WKUserContentController*)userContentController didReceiveScriptMessage:(WKScriptMessage*)message {if([message.name isEqualToString:@"currentCookies"]) {NSString*cookiesStr = message.body;//message.body返回的是一個(gè)id類型的對(duì)象,所以可以支持很多種js的參數(shù)類型(js的function除外)NSLog(@"當(dāng)前的cookie為: %@", cookiesStr);? ? }}

當(dāng)然,記得在適當(dāng)?shù)牡胤秸{(diào)用removeScriptMessageHandler

- (void)dealloc {//記得移除[self.webView.configuration.userContentControllerremoveScriptMessageHandlerForName:@"currentCookies"];}

這樣就完成了一次完整的JS -> OC的交互。

問(wèn)題

該方法還是沒(méi)有辦法直接獲取返回值。

通過(guò)window.webkit.messageHandlers..postMessage()傳遞的messageBody中不能包含js的function,如果包含了function,那么 OC端將不會(huì)收到回調(diào)

對(duì)于問(wèn)題1,我們可以采用異步回調(diào)的方式,將返回值返回給js。對(duì)于問(wèn)題2,一般js的參數(shù)中包含function是為了異步回調(diào),這里我們可以把js的function轉(zhuǎn)換為字符串,再傳遞給OC。

實(shí)際運(yùn)用

關(guān)于上述問(wèn)題1和問(wèn)題2的結(jié)合利用,實(shí)現(xiàn)JS -> OC的調(diào)用,并且OC -> JS 異步回調(diào)結(jié)果,這里還是拿分享來(lái)舉個(gè)例子。

比如js端實(shí)現(xiàn)了如下的方法(這段js的封裝前面的文章里也有提及,小伙伴有問(wèn)題可以看下之前的):

/**

* 分享方法,并且會(huì)異步回調(diào)分享結(jié)果

* @param? {對(duì)象類型} shareData 一個(gè)分享數(shù)據(jù)的對(duì)象,包含title,imgUrl,link以及一個(gè)回調(diào)function

* @return {void}? ? 無(wú)同步返回值

*/functionshareNew(shareData){//這是該方法的默認(rèn)實(shí)現(xiàn),上篇文章中有所提及vartitle = shareData.title;varimgUrl = shareData.imgUrl;varlink = shareData.link;varresult = shareData.result;//do something//這里模擬異步操作setTimeout(function(){//2s之后,回調(diào)true分享成功result(true);? ? ? },2000);//用于WKWebView,因?yàn)閃KWebView并沒(méi)有辦法把js function傳遞過(guò)去,因此需要特殊處理一下//把js function轉(zhuǎn)換為字符串,oc端調(diào)用時(shí) ()(true); 即可shareData.result = result.toString();window.webkit.messageHandlers.shareNew.postMessage(shareData);? }functiontest(){//清空分享結(jié)果shareResult.innerHTML ="";//調(diào)用時(shí),應(yīng)該shareNew({title:"title",imgUrl:"http://img.dd.com/xxx.png",link: location.href,result:function(res){//這里shareResult 等同于 document.getElementById("shareResult")shareResult.innerHTML = res ?"success":"failure";? ? ? ? ? }? ? ? });? }

在html頁(yè)面中我定義了一個(gè)a標(biāo)簽來(lái)觸發(fā)test()函數(shù)

測(cè)試新分享

在OC端,實(shí)現(xiàn)如下

//首先別忘了,在configuration中的userContentController中添加scriptMessageHandler[controller addScriptMessageHandler:selfname:@"shareNew"];//記得適當(dāng)時(shí)候remove哦//點(diǎn)擊a標(biāo)簽時(shí),則會(huì)調(diào)用下面的方法#pragma mark - WKScriptMessageHandler- (void)userContentController:(WKUserContentController*)userContentController didReceiveScriptMessage:(WKScriptMessage*)message {if([message.name isEqualToString:@"shareNew"]) {NSDictionary*shareData = message.body;NSLog(@"shareNew分享的數(shù)據(jù)為: %@", shareData);//模擬異步回調(diào)dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{//讀取js function的字符串NSString*jsFunctionString = shareData[@"result"];//拼接調(diào)用該方法的js字符串NSString*callbackJs = [NSStringstringWithFormat:@"(%@)(%d);", jsFunctionString,NO];//后面的參數(shù)NO為模擬分享失敗//執(zhí)行回調(diào)[self.webView evaluateJavaScript:callbackJs completionHandler:^(id_Nullable result,NSError* _Nullable error) {if(!error) {NSLog(@"模擬回調(diào),分享失敗");? ? ? ? ? ? ? ? }? ? ? ? ? ? }];? ? ? ? });? ? }}

那么當(dāng)我點(diǎn)擊a標(biāo)簽時(shí),html頁(yè)面上過(guò)2s,會(huì)顯示success,然后再過(guò)2s,會(huì)顯示failure。

new

我們來(lái)簡(jiǎn)單分析一下,點(diǎn)擊之后,觸發(fā)了test()函數(shù),test()中封裝了對(duì)share()函數(shù)的調(diào)用,且傳了一個(gè)對(duì)象作為參數(shù),對(duì)象中result字段對(duì)應(yīng)的是個(gè)匿名函數(shù),緊接著share()函數(shù)調(diào)用,其中的實(shí)現(xiàn)是2s過(guò)后,result(true);模擬js異步實(shí)現(xiàn)異步回調(diào)結(jié)果,分享成功。同時(shí)share()函數(shù)中,因?yàn)橥ㄟ^(guò)scriptMessageHandler無(wú)法傳遞function,所以先把shareData對(duì)象中的result這個(gè)匿名function轉(zhuǎn)成String,然后替換shareData對(duì)象的result屬性為這個(gè)String,并回傳給OC,OC這邊對(duì)應(yīng)JS對(duì)象的數(shù)據(jù)類型是NSDictionary,我們打印并得到了所有參數(shù),同時(shí),把result字段對(duì)應(yīng)的jsfunction String取出來(lái)。這里我們延遲4s回調(diào),模擬Native分享的異步過(guò)程,在4s后,也就是js中顯示success的2s過(guò)后,調(diào)用js的匿名function,并傳遞參數(shù)(分享結(jié)果)。調(diào)用一個(gè)js function的方法是functionName(argument);,這里由于這個(gè)js的function已經(jīng)是一個(gè)String了,所以我們調(diào)用時(shí),需要加上(),如(functionString)(argument);因此,最終我們通過(guò)OC -> JS 的evaluateJavaScript:completionHandler:方法,成功完成了異步回調(diào),并傳遞給js一個(gè)分享失敗的結(jié)果。

上面的描述看起來(lái)很復(fù)雜,其實(shí)就是先執(zhí)行了JS的默認(rèn)實(shí)現(xiàn),后執(zhí)行了OC的實(shí)現(xiàn)。上面的代碼展示了如何解決scriptMessageHandler的兩個(gè)問(wèn)題,并且實(shí)現(xiàn)了一個(gè) JS -> OC、OC -> JS 完整的交互流程。

Cookie管理

比起UIWebView的自動(dòng)管理,WKWebView坑爹的Cookie管理,相信阻止了很多的嘗試者。許多小伙伴也許曾經(jīng)都想從UIWebView轉(zhuǎn)到WKWebView,但估計(jì)因?yàn)镃ookie的問(wèn)題,最終都放棄了,筆者折騰WKWebView的Cookie長(zhǎng)達(dá)多半年之久,也曾想放棄,但最終還是堅(jiān)持下來(lái)了,雖說(shuō)現(xiàn)在不敢說(shuō)完全掌握,至少也不影響正常使用了。

下面來(lái)說(shuō)幾點(diǎn)注意事項(xiàng):

WKWebView加載網(wǎng)頁(yè)得到的Cookie會(huì)同步到NSHTTPCookieStorage中(也許你看過(guò)一些文章說(shuō)不能同步,但筆者這里說(shuō)下,它真的會(huì),大家可以嘗試下,實(shí)踐出真知)。

WKWebView加載請(qǐng)求時(shí),不會(huì)同步NSHTTPCookieStorage中已有的Cookie(是的,最坑的地方)。

通過(guò)共用一個(gè)WKProcessPool并不能解決2中Cookie同步問(wèn)題,且可能會(huì)造成Cookie丟失。

結(jié)合自己的實(shí)踐和參考一些資料,筆者得到上面的結(jié)論。

關(guān)于如何操作NSHTTPCookieStorage,前面的文章中提到過(guò)了,本文不再贅述。對(duì)于問(wèn)題2,StackOverFlow上有些解答,但經(jīng)過(guò)實(shí)際嘗試,發(fā)現(xiàn)還是或多或少有一些問(wèn)題。

為了解決這個(gè)最為致命的Cookie問(wèn)題,需要的做的有以下幾點(diǎn):

解決首次加載Cookie帶不上問(wèn)題

在request的requestHeader中添加Cookie:

NSMutableURLRequest*request = [NSMutableURLRequestrequestWithURL:[NSURLURLWithString:@"http://www.baidu.com"]];NSArray*cookies = [NSHTTPCookieStoragesharedHTTPCookieStorage].cookies;//Cookies數(shù)組轉(zhuǎn)換為requestHeaderFieldsNSDictionary*requestHeaderFields = [NSHTTPCookierequestHeaderFieldsWithCookies:cookies];//設(shè)置請(qǐng)求頭request.allHTTPHeaderFields = requestHeaderFields;[self.webView loadRequest:request];

這樣,只要你保證sharedHTTPCookieStorage中你的Cookie存在,首次訪問(wèn)一個(gè)頁(yè)面,就不會(huì)有問(wèn)題。

解決后續(xù)Ajax請(qǐng)求Cookie丟失問(wèn)題

解決此問(wèn)題,也比較簡(jiǎn)單,添加WKUserScript。

/*!

*? 更新webView的cookie

*/- (void)updateWebViewCookie{WKUserScript* cookieScript = [[WKUserScriptalloc] initWithSource:[selfcookieString] injectionTime:WKUserScriptInjectionTimeAtDocumentStartforMainFrameOnly:NO];//添加Cookie[self.configuration.userContentController addUserScript:cookieScript];}- (NSString*)cookieString{NSMutableString*script = [NSMutableStringstring];for(NSHTTPCookie*cookiein[[NSHTTPCookieStoragesharedHTTPCookieStorage] cookies]) {// Skip cookies that will break our scriptif([cookie.value rangeOfString:@"'"].location !=NSNotFound) {continue;? ? ? ? }// Create a line that appends this cookie to the web view's document's cookies[script appendFormat:@"document.cookie='%@'; \n", cookie.da_javascriptString];? ? }returnscript;}@interfaceNSHTTPCookie(Utils)- (NSString*)da_javascriptString;@end@implementationNSHTTPCookie(Utils)- (NSString*)da_javascriptString{NSString*string = [NSStringstringWithFormat:@"%@=%@;domain=%@;path=%@",self.name,self.value,self.domain,self.path ?:@"/"];if(self.secure) {? ? ? ? string = [string stringByAppendingString:@";secure=true"];? ? }returnstring;}@end

同樣只要你保證sharedHTTPCookieStorage中你的Cookie存在,后續(xù)Ajax請(qǐng)求就不會(huì)有問(wèn)題。

解決跳轉(zhuǎn)新頁(yè)面時(shí)Cookie帶不過(guò)去問(wèn)題

即便你做到了上面兩點(diǎn),你會(huì)發(fā)現(xiàn),當(dāng)你點(diǎn)擊頁(yè)面上的某個(gè)鏈接,跳轉(zhuǎn)到新的頁(yè)面,Cookie又丟了,此時(shí)你是想狗帶的~怎么解決呢?

//核心方法:/**

修復(fù)打開(kāi)鏈接Cookie丟失問(wèn)題

@param request 請(qǐng)求

@return 一個(gè)fixedRequest

*/- (NSURLRequest*)fixRequest:(NSURLRequest*)request{NSMutableURLRequest*fixedRequest;if([request isKindOfClass:[NSMutableURLRequestclass]]) {? ? ? ? fixedRequest = (NSMutableURLRequest*)request;? ? }else{? ? ? ? fixedRequest = request.mutableCopy;? ? }//防止Cookie丟失NSDictionary*dict = [NSHTTPCookierequestHeaderFieldsWithCookies:[NSHTTPCookieStoragesharedHTTPCookieStorage].cookies];if(dict.count) {NSMutableDictionary*mDict = request.allHTTPHeaderFields.mutableCopy;? ? ? ? [mDict setValuesForKeysWithDictionary:dict];? ? ? ? fixedRequest.allHTTPHeaderFields = mDict;? ? }returnfixedRequest;}#pragma mark - WKNavigationDelegate- (void)webView:(WKWebView*)webView decidePolicyForNavigationAction:(WKNavigationAction*)navigationAction decisionHandler:(void(^)(WKNavigationActionPolicy))decisionHandler {#warning important 這里很重要//解決Cookie丟失問(wèn)題NSURLRequest*originalRequest = navigationAction.request;? ? [selffixRequest:originalRequest];//如果originalRequest就是NSMutableURLRequest, originalRequest中已添加必要的Cookie,可以跳轉(zhuǎn)//允許跳轉(zhuǎn)decisionHandler(WKNavigationActionPolicyAllow);//可能有小伙伴,會(huì)說(shuō)如果originalRequest是NSURLRequest,不可變,那不就添加不了Cookie了,是的,我們不能因?yàn)檫@個(gè)問(wèn)題,不允許跳轉(zhuǎn),也不能在不允許跳轉(zhuǎn)之后用loadRequest加載fixedRequest,否則會(huì)出現(xiàn)死循環(huán),具體的,小伙伴們可以用本地的html測(cè)試下。NSLog(@"%@",NSStringFromSelector(_cmd));}#pragma mark - WKUIDelegate- (WKWebView*)webView:(WKWebView*)webView createWebViewWithConfiguration:(WKWebViewConfiguration*)configuration forNavigationAction:(WKNavigationAction*)navigationAction windowFeatures:(WKWindowFeatures*)windowFeatures {#warning important 這里也很重要//這里不打開(kāi)新窗口[self.webView loadRequest:[selffixRequest:navigationAction.request]];returnnil;}

最終的方法,已經(jīng)附上。小伙伴們自行參考。同樣需要你保證sharedHTTPCookieStorage中你的Cookie存在。

解決上面3步都做了Cookie依然丟失

看過(guò)上面的方法過(guò)后,小伙伴們應(yīng)該記得最清楚的是保證sharedHTTPCookieStorage中你的Cookie存在。怎么保證呢?由于WKWebView加載網(wǎng)頁(yè)得到的Cookie會(huì)同步到NSHTTPCookieStorage中的特點(diǎn),有時(shí)候你強(qiáng)行添加的Cookie會(huì)在同步過(guò)程中丟失。抓包(Mac推薦Charles)你就會(huì)發(fā)現(xiàn),點(diǎn)擊一個(gè)鏈接時(shí),Request的header中多了Set-Cookie字段,其實(shí)Cookie已經(jīng)丟了。下面推薦筆者的解決方案,那就是把自己需要的Cookie主動(dòng)保存起來(lái),每次調(diào)用[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies方法時(shí),保證返回的數(shù)組中有自己需要的Cookie。下面上代碼,用了runtime的Method Swizzling,詳細(xì)代碼,請(qǐng)參考Demo

首先是在適當(dāng)?shù)臅r(shí)候,保存

//比如登錄成功,保存CookieNSArray*allCookies = [[NSHTTPCookieStoragesharedHTTPCookieStorage] cookies];for(NSHTTPCookie*cookieinallCookies) {if([cookie.name isEqualToString:DAServerSessionCookieName]) {NSDictionary*dict = [[NSUserDefaultsstandardUserDefaults] dictionaryForKey:DAUserDefaultsCookieStorageKey];if(dict) {NSHTTPCookie*localCookie = [NSHTTPCookiecookieWithProperties:dict];if(![cookie.value isEqual:localCookie.value]) {NSLog(@"本地Cookie有更新");? ? ? ? ? ? }? ? ? ? }? ? ? ? [[NSUserDefaultsstandardUserDefaults] setObject:cookie.properties forKey:DAUserDefaultsCookieStorageKey];? ? ? ? [[NSUserDefaultsstandardUserDefaults] synchronize];break;? ? }}

在讀取時(shí),如果沒(méi)有則添加

@implementationNSHTTPCookieStorage(Utils)+ (void)load{? ? class_methodSwizzling(self,@selector(cookies),@selector(da_cookies));}- (NSArray *)da_cookies{NSArray*cookies = [selfda_cookies];BOOLisExist =NO;for(NSHTTPCookie*cookieincookies) {if([cookie.name isEqualToString:DAServerSessionCookieName]) {? ? ? ? ? ? isExist =YES;break;? ? ? ? }? ? }if(!isExist) {//CookieStroage中添加NSDictionary*dict = [[NSUserDefaultsstandardUserDefaults] dictionaryForKey:DAUserDefaultsCookieStorageKey];if(dict) {NSHTTPCookie*cookie = [NSHTTPCookiecookieWithProperties:dict];? ? ? ? ? ? [[NSHTTPCookieStoragesharedHTTPCookieStorage] setCookie:cookie];NSMutableArray*mCookies = cookies.mutableCopy;? ? ? ? ? ? [mCookies addObject:cookie];? ? ? ? ? ? cookies = mCookies.copy;? ? ? ? }? ? }returncookies;}@end

當(dāng)打開(kāi)手機(jī)百度首頁(yè)后,我們查看頁(yè)面中的Cookie

其中第一個(gè),是之前測(cè)試添加的,用來(lái)動(dòng)態(tài)注入js。

WKUserScript *newCookieScript = [[WKUserScript alloc]initWithSource:@"document.cookie = 'DarkAngelCookie=DarkAngel;'"injectionTime:WKUserScriptInjectionTimeAtDocumentStartforMainFrameOnly:NO];[controlleraddUserScript:newCookieScript];

第二個(gè),就是真正有用的Cookie啦,這幅圖用到了Safari調(diào)試,后面會(huì)講到。通過(guò)上面的折騰,一般,就能夠有效減少Cookie的丟失了。

性能對(duì)比

加載一般的頁(yè)面,對(duì)比不出什么,這里我就測(cè)試下內(nèi)存占用吧,同樣一個(gè)html,分布看下內(nèi)存占用。

UIWebView

WKWebView

從頁(yè)面UI元素上看,WKWebView還多個(gè)barButtonItem呢,這么簡(jiǎn)單個(gè)頁(yè),內(nèi)存占用小了3M,復(fù)雜的頁(yè)面可想而知。

各種坑

雖然WKWebView真的很不錯(cuò),但是它的坑,還是有很多的,下面簡(jiǎn)單說(shuō)下。

js alert方法不彈窗

之前提過(guò)WKUIDelegate所有的方法都是Optional,但如果你不實(shí)現(xiàn),它就會(huì)

If you do not implement this method, the web view will behave as if the user selected the OK button.

- (void)webView:(WKWebView *)webViewrunJavaScriptAlertPanelWithMessage:(NSString *)messageinitiatedByFrame:(WKFrameInfo *)framecompletionHandler:(void(^)(void))completionHandler;

OK,意思就是說(shuō),如果不實(shí)現(xiàn),就什么都不發(fā)生,好吧,乖乖實(shí)現(xiàn)吧,實(shí)現(xiàn)了就能彈窗了。

白屏問(wèn)題

當(dāng)WKWebView加載的網(wǎng)頁(yè)占用內(nèi)存過(guò)大時(shí),會(huì)出現(xiàn)白屏現(xiàn)象。解決方案是

/*! @abstract Invoked when the web view's web content process is terminated.

@param webView The web view whose underlying web content process was terminated.

*/-(void)webViewWebContentProcessDidTerminate:(WKWebView*)webView{[webView reload];//刷新就好了}

有時(shí)白屏,不會(huì)調(diào)用該方法,具體的解決方案是

比如,最近遇到在一個(gè)高內(nèi)存消耗的H5頁(yè)面上 present 系統(tǒng)相機(jī),拍照完畢后返回原來(lái)頁(yè)面的時(shí)候出現(xiàn)白屏現(xiàn)象(拍照過(guò)程消耗了大量?jī)?nèi)存,導(dǎo)致內(nèi)存緊張,WebContent Process 被系統(tǒng)掛起),但上面的回調(diào)函數(shù)并沒(méi)有被調(diào)用。在WKWebView白屏的時(shí)候,另一種現(xiàn)象是 webView.titile 會(huì)被置空, 因此,可以在 viewWillAppear 的時(shí)候檢測(cè) webView.title 是否為空來(lái) reload 頁(yè)面。(出自WKWebView 那些坑

Cookie丟失

從一個(gè)登錄狀態(tài)的頁(yè)面跳轉(zhuǎn)到另一個(gè)頁(yè)面,WTF,登錄狀態(tài)丟失了?什么鬼?其實(shí)上文中的Cookie管理一節(jié),已經(jīng)介紹過(guò)解決方案了,原因也就是WKWebView加載請(qǐng)求時(shí),不會(huì)同步NSHTTPCookieStorage中已有的Cookie。如果偶爾還是會(huì)出現(xiàn)丟失登錄狀態(tài)的情況,那筆者只能說(shuō),再檢查下自己的代碼,找找原因,有好的解決方案,歡迎告知筆者。

evaluateJavaScript:completionHandler:異步

該方法是異步回調(diào),這個(gè)一看方法的聲明便知??赡苡行』锇榫褪切枰将@取返回值,有沒(méi)有辦法呢?答案是沒(méi)有。

可能你會(huì)說(shuō)用信號(hào)量dispatch_semaphore_t。好吧,可能你會(huì)這么寫~

__blockidcookies;dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);[self.webView evaluateJavaScript:@"document.cookie"completionHandler:^(id_Nullable result,NSError* _Nullable error) {? ? cookies = result;? ? dispatch_semaphore_signal(semaphore);}];//等待三秒,接收參數(shù)dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW,3*NSEC_PER_SEC));//打印cookie,肯定為空,因?yàn)樽阕愕攘?s,dispatch_semaphore_signal都沒(méi)有起作用NSLog(@"cookie的值為:%@", cookies);

筆者故意只等待了3s,如果你等待DISPATCH_TIME_FOREVER,恭喜你,程序不會(huì)Crash,但界面卡死了。筆者測(cè)試的結(jié)果是,NSLog的觸發(fā)時(shí)間要早于completionHandler回調(diào),不論你等多久,它都會(huì)打印null。所以當(dāng)你永久等待時(shí),就卡死了。這里的緣由,筆者不太清楚,有搞清楚的小伙伴可以幫忙指點(diǎn)一下,謝謝~

所以還是老實(shí)的接受異步回調(diào)吧,不要用信號(hào)來(lái)搞成同步,會(huì)卡死的。

自定義contentInset刷新時(shí)頁(yè)面跳動(dòng)的bug

PM說(shuō)毛玻璃好看,,so easy,于是我們?cè)诖a中輕輕敲下

self.webView.scrollView.contentInset = UIEdgeInsetsMake(64,0,49,0);

然后默默的微笑著點(diǎn)擊cmd + R,太簡(jiǎn)單了。然后看到了這樣的畫面

是的,上面的方法在UIWebView中沒(méi)毛病,可是在WKWebView中,就產(chǎn)生了刷新時(shí)頁(yè)面跳動(dòng)的bug。

這個(gè)坑,坑了我大半年之久,Apple的Document中沒(méi)有記錄,最終筆者在Apple開(kāi)源的WebKit2ChangeLog中找到了答案。下面是官方人員的回答:

厲害了,word哥,我選擇狗帶,居然還是私有Api。怎么整呢?

self.webView.scrollView.contentInset = UIEdgeInsetsMake(64,0,49,0);//史詩(shī)級(jí)神坑,為何如此寫呢?參考https://opensource.apple.com/source/WebKit2/WebKit2-7600.1.4.11.10/ChangeLog? [self.webViewsetValue:[NSValuevalueWithUIEdgeInsets:self.webView.scrollView.contentInset]forKey:@"_obscuredInsets"];//kvc給WKWebView的私有變量_obscuredInsets設(shè)置值

這么寫就OK了,通過(guò)KVC設(shè)置私有變量的值,筆者用了半年了,過(guò)Apple審核沒(méi)問(wèn)題,不用擔(dān)心。如果這個(gè)能幫助到大家,不用感謝我~

加載POST請(qǐng)求丟失RequestBody

這個(gè)問(wèn)題,沒(méi)有直接的解決辦法。問(wèn)題的根源在于:

在 webkit2 的設(shè)計(jì)里使用 MessageQueue 進(jìn)行進(jìn)程之間的通信,Network Process 會(huì)將請(qǐng)求 encode 成一個(gè) Message,然后通過(guò) IPC 發(fā)送給 App Process。出于性能的原因,encode 的時(shí)候 HTTPBody 和 HTTPBodyStream 這兩個(gè)字段被丟棄掉了。

因此,如果通過(guò) registerSchemeForCustomProtocol 注冊(cè)了 http(s) scheme, 那么由 WKWebView 發(fā)起的所有 http(s)請(qǐng)求都會(huì)通過(guò) IPC 傳給主進(jìn)程 NSURLProtocol 處理,導(dǎo)致 post 請(qǐng)求 body 被清空。

(出自WKWebView 那些坑

參考Apple源碼bug report。

具體的解決辦法,就是另辟蹊徑,WKWebView 那些坑中有介紹,這里筆者不再展開(kāi)。

因?yàn)閃KWebView被設(shè)計(jì)的使用場(chǎng)景,是用來(lái)當(dāng)做瀏覽器,解決Native可以直接在App內(nèi)瀏覽網(wǎng)頁(yè)的問(wèn)題。而瀏覽器瀏覽一個(gè)網(wǎng)站,怎么可能是POST請(qǐng)求呢?所以這個(gè)問(wèn)題,筆者目前感受較小,有需要的小伙伴可以自行解決。

NSURLProtocol問(wèn)題

WKWebView不同于UIWebView,其實(shí)并不支持NSURLProtocol。如果想攔截,可以通過(guò)調(diào)用私有Api。

+[WKBrowsingContextController registerSchemeForCustomProtocol:]

此方法缺點(diǎn)也很多,筆者這里不推薦小伙伴使用,畢竟調(diào)用私有Api是Apple禁止的。況且,真的必須使用NSURLProtocol的話,還是用UIWebView吧。

未完待續(xù)

本文主要講述了WKWebView的一些基礎(chǔ)用法、OC與JS的交互,Cookie的管理,以及一些使用過(guò)程中的坑,旨在為沒(méi)用過(guò)的小伙伴們?cè)敿?xì)介紹下。雖然它的坑很多,但是它的優(yōu)點(diǎn)也有很多,我們應(yīng)該敢于擁抱新事物,擁抱新知識(shí)。還在等什么?WKWebView趕快用起來(lái)吧~

下篇文章,將主要為小伙伴們介紹下如何用Safari調(diào)試,實(shí)際應(yīng)用中一些需求如何實(shí)現(xiàn),如何更好的與前端h5開(kāi)發(fā)同學(xué)配合以及如何找出問(wèn)題所在等。下篇文章見(jiàn)~

下篇文章已發(fā)布:

iOS中UIWebView與WKWebView、JavaScript與OC交互、Cookie管理看我就夠(下)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容