WKWebview 處理

前言

關于UIWebView的介紹,相信看過上文的小伙伴們,已經大概清楚了吧,如果有問題,歡迎提問。

本文是本系列文章的第二篇,主要為小伙伴們分享下WKWebView相關的內容:

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

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

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

關于文中提到的一些內容,這里我準備了個Demo,有需要的小伙伴可以下載。

本文目錄

前言

WKWebView

簡介

基本用法

創建

動態注入js

加載

代理

新屬性

JavaScript與Objective-C的交互

OC -> JS

JS -> OC

URL攔截

scriptMessageHandler

實際運用

Cookie管理

解決首次加載Cookie帶不上問題

解決后續Ajax請求Cookie丟失問題

解決跳轉新頁面時Cookie帶不過去問題

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

性能對比

各種坑

js alert方法不彈窗

白屏問題

Cookie丟失

evaluateJavaScript:completionHandler:異步

自定義contentInset刷新時頁面跳動的bug

加載POST請求丟失RequestBody

NSURLProtocol問題

未完待續

WKWebView

簡介

WKWebView是Apple于iOS 8.0推出的WebKit中的核心控件,用來替代UIWebView。WKWebView比UIWebView的優勢在于:

更多的支持HTML5的特性

高達60fps的滾動刷新率以及內置手勢

與Safari相同的JavaScript引擎

將UIWebViewDelegate與UIWebView拆分成了14類與3個協議(官方文檔說明

可以獲取加載進度:estimatedProgress(UIWebView需要調用私有Api)

作者本人在項目中使用WKWebView也1年多了,確確實實感受到了它的優勢,但是同樣也感受到了它帶來的一些坑。下面來具體的介紹下WKWebView。其實Apple開源了WebKit,有興趣的小伙伴可以研究下它的實現。

基本用法

創建

WKWebView的創建方法有這兩種

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

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

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

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

第一個屬性是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));

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

第二個屬性是userContentController。

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

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

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

那么整體我創建一個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;//允許右滑返回上個鏈接,左滑前進self.webView.allowsLinkPreview =YES;//允許鏈接3D Touchself.webView.customUserAgent =@"WebViewDemo/1.0.0";//自定義UA,UIWebView就沒有此功能,后面會講到通過其他方式實現self.webView.UIDelegate =self;self.webView.navigationDelegate =self;[self.view addSubview:self.webView];

動態注入js

通過給userContentController添加WKUserScript,可以實現動態注入js。比如我先注入一個腳本,給每個頁面添加一個Cookie

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

然后再注入一個腳本,每當頁面加載,就會alert當前頁面cookie,在OC中的實現

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

這樣每當頁面出現的時候,會alet彈出當前頁面所有的cookie字符串。

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

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

加載

加載一個請求或者頁面也很簡單

- (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的很相似,但是需要說明的是,加載本地的一個html需要使用loadRequest:方法,使用loadHTMLString:baseURL:方法會有問題。

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

代理

在WKWebView的頭文件,你會發現

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

有兩個協議,它將UIWebView的代理協議拆成了一個跳轉的協議和一個關于UI的協議。雖說這兩個協議中的所有方法都是Optional,但是關于WKUIDelegate協議是有坑的,后面的各種坑中會提到。簡單說下WKNavigationDelegate中比較常用的方法

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

WKUIDelegate這里先不提了,小伙伴們可以參考我Demo中的實現。

新屬性

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

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

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

JavaScript與Objective-C的交互

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

OC -> JS

這個比較簡單,WKWebView提供了一個類似JavaScriptCore的方法

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

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

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

JS -> OC

URL攔截

此方法與上篇文章中UIWebView介紹到的URL攔截方法一致,都是通過自定義Scheme,在鏈接激活時,攔截該URL,拿到參數,調用OC方法,缺點依然明顯。WKWebView實現起來如下:

比如我的鏈接依然是

短信驗證登錄

當用戶點擊這個a標簽時,會被攔截

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

整體實現是與UIWebView十分相似的,這里就不多說了。

這里再次提一下WebViewJavascriptBridge,它在最近的新版本中支持了WKWebView。使用的方案同樣是攔截URL,具體原理在之前的文章中簡單描述過,這里不再贅述。下面說下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;

其實Apple的注釋已經很清楚了,在OC中添加一個scriptMessageHandler,則會在all frames中添加一個js的function:window.webkit.messageHandlers..postMessage()。那么當我在OC中通過如下的方法添加了一個handler,如

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

則當我在js中調用下面的方法時

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

我在OC中將會收到WKScriptMessageHandler的回調

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

當然,記得在適當的地方調用removeScriptMessageHandler

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

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

問題

該方法還是沒有辦法直接獲取返回值

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

對于問題1,我們可以采用異步回調的方式,將返回值返回給js。對于問題2,一般js的參數中包含function是為了異步回調,這里我們可以把js的function轉換為字符串,再傳遞給OC。

實際運用

關于上述問題1和問題2的結合利用,實現JS -> OC的調用,并且OC -> JS 異步回調結果,這里還是拿分享來舉個例子。

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

/**

* 分享方法,并且會異步回調分享結果

* @param? {對象類型} shareData 一個分享數據的對象,包含title,imgUrl,link以及一個回調function

* @return {void}? ? 無同步返回值

*/functionshareNew(shareData){//這是該方法的默認實現,上篇文章中有所提及vartitle = shareData.title;varimgUrl = shareData.imgUrl;varlink = shareData.link;varresult = shareData.result;//do something//這里模擬異步操作setTimeout(function(){//2s之后,回調true分享成功result(true);? ? ? },2000);//用于WKWebView,因為WKWebView并沒有辦法把js function傳遞過去,因此需要特殊處理一下//把js function轉換為字符串,oc端調用時 ()(true); 即可shareData.result = result.toString();window.webkit.messageHandlers.shareNew.postMessage(shareData);? }functiontest(){//清空分享結果shareResult.innerHTML ="";//調用時,應該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頁面中我定義了一個a標簽來觸發test()函數

測試新分享

在OC端,實現如下

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

那么當我點擊a標簽時,html頁面上過2s,會顯示success,然后再過2s,會顯示failure

new

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

上面的描述看起來很復雜,其實就是先執行了JS的默認實現,后執行了OC的實現。上面的代碼展示了如何解決scriptMessageHandler的兩個問題,并且實現了一個 JS -> OC、OC -> JS 完整的交互流程。

Cookie管理

比起UIWebView的自動管理,WKWebView坑爹的Cookie管理,相信阻止了很多的嘗試者。許多小伙伴也許曾經都想從UIWebView轉到WKWebView,但估計因為Cookie的問題,最終都放棄了,筆者折騰WKWebView的Cookie長達多半年之久,也曾想放棄,但最終還是堅持下來了,雖說現在不敢說完全掌握,至少也不影響正常使用了。

下面來說幾點注意事項:

WKWebView加載網頁得到的Cookie會同步到NSHTTPCookieStorage中(也許你看過一些文章說不能同步,但筆者這里說下,它真的會,大家可以嘗試下,實踐出真知)。

WKWebView加載請求時,不會同步NSHTTPCookieStorage中已有的Cookie(是的,最坑的地方)。

通過共用一個WKProcessPool并不能解決2中Cookie同步問題,且可能會造成Cookie丟失。

結合自己的實踐和參考一些資料,筆者得到上面的結論。

關于如何操作NSHTTPCookieStorage,前面的文章中提到過了,本文不再贅述。對于問題2,StackOverFlow上有些解答,但經過實際嘗試,發現還是或多或少有一些問題。

為了解決這個最為致命的Cookie問題,需要的做的有以下幾點:

解決首次加載Cookie帶不上問題

在request的requestHeader中添加Cookie:

NSMutableURLRequest*request = [NSMutableURLRequestrequestWithURL:[NSURLURLWithString:@"http://www.baidu.com"]];NSArray*cookies = [NSHTTPCookieStoragesharedHTTPCookieStorage].cookies;//Cookies數組轉換為requestHeaderFieldsNSDictionary*requestHeaderFields = [NSHTTPCookierequestHeaderFieldsWithCookies:cookies];//設置請求頭request.allHTTPHeaderFields = requestHeaderFields;[self.webView loadRequest:request];

這樣,只要你保證sharedHTTPCookieStorage中你的Cookie存在,首次訪問一個頁面,就不會有問題。

解決后續Ajax請求Cookie丟失問題

解決此問題,也比較簡單,添加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存在,后續Ajax請求就不會有問題。

解決跳轉新頁面時Cookie帶不過去問題

即便你做到了上面兩點,你會發現,當你點擊頁面上的某個鏈接,跳轉到新的頁面,Cookie又丟了,此時你是想狗帶的~怎么解決呢?

//核心方法:/**

修復打開鏈接Cookie丟失問題

@param request 請求

@return 一個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丟失問題NSURLRequest*originalRequest = navigationAction.request;? ? [selffixRequest:originalRequest];//如果originalRequest就是NSMutableURLRequest, originalRequest中已添加必要的Cookie,可以跳轉//允許跳轉decisionHandler(WKNavigationActionPolicyAllow);//可能有小伙伴,會說如果originalRequest是NSURLRequest,不可變,那不就添加不了Cookie了,是的,我們不能因為這個問題,不允許跳轉,也不能在不允許跳轉之后用loadRequest加載fixedRequest,否則會出現死循環,具體的,小伙伴們可以用本地的html測試下。NSLog(@"%@",NSStringFromSelector(_cmd));}#pragma mark - WKUIDelegate- (WKWebView*)webView:(WKWebView*)webView createWebViewWithConfiguration:(WKWebViewConfiguration*)configuration forNavigationAction:(WKNavigationAction*)navigationAction windowFeatures:(WKWindowFeatures*)windowFeatures {#warning important 這里也很重要//這里不打開新窗口[self.webView loadRequest:[selffixRequest:navigationAction.request]];returnnil;}

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

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

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

首先是在適當的時候,保存

//比如登錄成功,保存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;? ? }}

在讀取時,如果沒有則添加

@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

當打開手機百度首頁后,我們查看頁面中的Cookie

其中第一個,是之前測試添加的,用來動態注入js。

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

第二個,就是真正有用的Cookie啦,這幅圖用到了Safari調試,后面會講到。通過上面的折騰,一般,就能夠有效減少Cookie的丟失了。

性能對比

加載一般的頁面,對比不出什么,這里我就測試下內存占用吧,同樣一個html,分布看下內存占用。

UIWebView

WKWebView

從頁面UI元素上看,WKWebView還多個barButtonItem呢,這么簡單個頁,內存占用小了3M,復雜的頁面可想而知。

各種坑

雖然WKWebView真的很不錯,但是它的坑,還是有很多的,下面簡單說下。

js alert方法不彈窗

之前提過WKUIDelegate所有的方法都是Optional,但如果你不實現,它就會

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,意思就是說,如果不實現,就什么都不發生,好吧,乖乖實現吧,實現了就能彈窗了。

白屏問題

當WKWebView加載的網頁占用內存過大時,會出現白屏現象。解決方案是

/*! @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];//刷新就好了}

有時白屏,不會調用該方法,具體的解決方案是

比如,最近遇到在一個高內存消耗的H5頁面上 present 系統相機,拍照完畢后返回原來頁面的時候出現白屏現象(拍照過程消耗了大量內存,導致內存緊張,WebContent Process 被系統掛起),但上面的回調函數并沒有被調用。在WKWebView白屏的時候,另一種現象是 webView.titile 會被置空, 因此,可以在 viewWillAppear 的時候檢測 webView.title 是否為空來 reload 頁面。(出自WKWebView 那些坑

Cookie丟失

從一個登錄狀態的頁面跳轉到另一個頁面,WTF,登錄狀態丟失了?什么鬼?其實上文中的Cookie管理一節,已經介紹過解決方案了,原因也就是WKWebView加載請求時,不會同步NSHTTPCookieStorage中已有的Cookie。如果偶爾還是會出現丟失登錄狀態的情況,那筆者只能說,再檢查下自己的代碼,找找原因,有好的解決方案,歡迎告知筆者。

evaluateJavaScript:completionHandler:異步

該方法是異步回調,這個一看方法的聲明便知。可能有小伙伴就是需要同步獲取返回值,有沒有辦法呢?答案是沒有

可能你會說用信號量dispatch_semaphore_t。好吧,可能你會這么寫~

__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);}];//等待三秒,接收參數dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW,3*NSEC_PER_SEC));//打印cookie,肯定為空,因為足足等了3s,dispatch_semaphore_signal都沒有起作用NSLog(@"cookie的值為:%@", cookies);

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

所以還是老實的接受異步回調吧,不要用信號來搞成同步,會卡死的。

自定義contentInset刷新時頁面跳動的bug

PM說毛玻璃好看,,so easy,于是我們在代碼中輕輕敲下

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

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

是的,上面的方法在UIWebView中沒毛病,可是在WKWebView中,就產生了刷新時頁面跳動的bug。

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

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

self.webView.scrollView.contentInset = UIEdgeInsetsMake(64,0,49,0);//史詩級神坑,為何如此寫呢?參考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設置值

這么寫就OK了,通過KVC設置私有變量的值,筆者用了半年了,過Apple審核沒問題,不用擔心。如果這個能幫助到大家,不用感謝我~

加載POST請求丟失RequestBody

這個問題,沒有直接的解決辦法。問題的根源在于:

在 webkit2 的設計里使用 MessageQueue 進行進程之間的通信,Network Process 會將請求 encode 成一個 Message,然后通過 IPC 發送給 App Process。出于性能的原因,encode 的時候 HTTPBody 和 HTTPBodyStream 這兩個字段被丟棄掉了。

因此,如果通過 registerSchemeForCustomProtocol 注冊了 http(s) scheme, 那么由 WKWebView 發起的所有 http(s)請求都會通過 IPC 傳給主進程 NSURLProtocol 處理,導致 post 請求 body 被清空

(出自WKWebView 那些坑

參考Apple源碼bug report

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

因為WKWebView被設計的使用場景,是用來當做瀏覽器,解決Native可以直接在App內瀏覽網頁的問題。而瀏覽器瀏覽一個網站,怎么可能是POST請求呢?所以這個問題,筆者目前感受較小,有需要的小伙伴可以自行解決。

NSURLProtocol問題

WKWebView不同于UIWebView,其實并不支持NSURLProtocol。如果想攔截,可以通過調用私有Api。

+[WKBrowsingContextController registerSchemeForCustomProtocol:]

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

未完待續

本文主要講述了WKWebView的一些基礎用法、OC與JS的交互,Cookie的管理,以及一些使用過程中的坑,旨在為沒用過的小伙伴們詳細介紹下。雖然它的坑很多,但是它的優點也有很多,我們應該敢于擁抱新事物,擁抱新知識。還在等什么?WKWebView趕快用起來吧~

下篇文章,將主要為小伙伴們介紹下如何用Safari調試,實際應用中一些需求如何實現,如何更好的與前端h5開發同學配合以及如何找出問題所在等。下篇文章見~

下篇文章已發布:

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容