探祕WKWebView

劉小壯發表於2021-11-01

概述

之前主要使用UIWebView進行頁面的載入,但是UIWebView存在很多問題,在2020年已經被蘋果正式拋棄。所以本篇文章主要講解WKWebViewWKWebViewiOS8開始支援,現在大多數App應該都不支援iOS7了。

UIWebView存在兩個問題,一個是記憶體消耗比較大,另一個是效能很差。WKWebView相對於UIWebView來說,效能要比UIWebView效能要好太多,重新整理率能達到60FPS。記憶體佔用也比UIWebView要小。

WKWebView是一個多程式元件,NetworkUI Render都在獨立的程式中完成。

由於WKWebViewApp不在同一個程式,如果WKWebView程式崩潰並不會導致應用崩潰,僅僅是頁面白屏等異常。頁面的載入、渲染等消耗記憶體和效能的操作,都在WKWebView的程式中處理,處理後再將結果交給App程式用於顯示,所以App程式的效能消耗會小很多。

網頁載入流程

  1. 通過域名的方式請求伺服器,請求前瀏覽器會做一個DNS解析,並將IP地址返回給瀏覽器。
  2. 瀏覽器使用IP地址請求伺服器,並且開始握手過程。TCP是三次握手,如果使用https則還需要進行TLS的握手,握手後根據協議欄位選擇是否保持連線。
  3. 握手完成後,瀏覽器向服務端傳送請求,獲取html檔案。
  4. 伺服器解析請求,並由CDN伺服器返回對應的資原始檔。
  5. 瀏覽器收到伺服器返回的html檔案,交由html解析器進行解析。
  6. 解析html由上到下進行解析xml標籤,過程中如果遇到css或資原始檔,都會進行非同步載入,遇到js則會掛起當前html解析任務,請求js並返回後繼續解析。因為js檔案可能會對DOM樹進行修改。
  7. 解析完html,並執行完js程式碼,形成最終的DOM樹。通過DOM配合css檔案找出每個節點的最終展示樣式,並交由瀏覽器進行渲染展示
  8. 結束連結。

代理方法

WKWebViewUIWebView的代理方法發生了一些改變,WKWebView的流程更加細化了。例如之前UI結束請求後,會立刻渲染到webView上。而WKWebView則會在渲染到螢幕之前,會回撥一個代理方法,代理方法決定是否渲染到螢幕上。這樣就可以對請求下來的資料做一次校驗,防止資料被更改,或驗證檢視是否允許被顯示到螢幕上。

除此之外,WKWebView相對於UIWebView還多了一些定製化操作。

  1. 重定向的回撥,可以在請求重定向時獲取到這次操作。
  2. WKWebView程式異常退出時,可以通過回撥獲取。
  3. 自定義處理證照。
  4. 更深層的UI定製操作,將alertUI操作交給原生層面處理,而UI方案UIAlertView是直接webView顯示的。

WKUIDelegate

WKWebView將很多UI的顯示都交給原生層面去處理,例如彈窗或者輸入框的顯示。這樣如果專案裡有統一定義的彈窗,就可以直接呼叫自定義彈窗,而不是隻能展示系統彈窗。

WKWebView中,系統將彈窗的顯示交由客戶端來控制。客戶端可以通過下面的回撥方法獲取到彈窗的顯示資訊,並由客戶端來調起UIAlertController來展示。引數中有一個completionHandler的回撥block,需要客戶端一定要呼叫,如果不呼叫則會發生崩潰。

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;

有時候H5會要求使用者進行一些輸入,例如使用者名稱密碼之類的。客戶端可以通過下面的方法獲取到輸入框事件,並由客戶端展示輸入框,使用者輸入完成後將結果回撥給completionHandler中。

- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler;

WKNavigationDelegate

關於載入流程相關的方法,都被抽象到WKNavigationDelegate中,這裡挑幾個比較常用的方法講一下。

下面的方法,通過decisionHandler回撥中返回一個列舉型別的引數,表示是否允許頁面載入。這裡可以對域名進行判斷,如果是站外域名,則可以提示使用者是否進行跳轉。如果是跳轉其他App或商店的URL,則可以通過openURL進行跳轉,並將這次請求攔截。包括cookie的處理也在此方法中完成,後面會詳細講到cookie的處理。

除此之外,很多頁面顯示前的邏輯處理,也在此方法中完成。但需要注意的是,方法中不要做過多的耗時處理,會影響頁面載入速度。

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

開始載入頁面,並請求伺服器。

- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;

當頁面載入失敗的時候,會回撥此方法,包括timeout等錯誤。在這個頁面可以展示錯誤頁面,清空進度條,重置網路指示器等操作。需要注意的是,呼叫goBack時也會執行此方法,可以通過error的狀態判斷是否NSURLErrorCancelled來過濾掉。

- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error;

頁面載入及渲染完成,會呼叫此方法,呼叫此方法時H5dom已經解析並渲染完成,展示在螢幕上。所以在此方法中可以進行一些載入完成的操作,例如移除進度條,重置網路指示器等。

- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;

WKUserContentController

回撥

WKWebView將和js的互動都由WKUserContentController類來處理,後面統稱為userContent

如果需要接收並處理js的呼叫,通過呼叫addScriptMessageHandler:name:方法,並傳入一個實現了WKScriptMessageHandler協議的物件,即可接收js的回撥,由於userContent會強引用傳入的物件,所以應該是新建立一個物件,而不是self。註冊物件時,後面的name就是js呼叫的函式名。

WKUserContentController *userContent = [[WKUserContentController alloc] init];
[userContent addScriptMessageHandler:[[WKWeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"clientCallback"];

dealloc中應該通過下面的方法,移除對指定name的處理。

[userContent removeScriptMessageHandlerForName:@"clientCallback"];

H5通過下面的程式碼即可對客戶端發起呼叫,呼叫是通過postMessage函式傳一個json串過來,需要加上轉移字元。客戶端接收到呼叫後,根據回撥方法傳入的WKScriptMessage物件,獲取到body字典,解析傳入的引數即可。

window.webkit.messageHandlers.clientCallback.postMessage("{\"funName\":\"getMobileCode\",\"value\":\"srggshqisslfkj\"}");

呼叫

原生呼叫H5的方法也是一樣,建立一個WKUserScript物件,並將js程式碼當做引數傳入。除了呼叫js程式碼,也可以通過此方法注入程式碼改變頁面dom,但是這樣程式碼量較大,不建議這麼做。

WKUserScript *wkcookieScript = [[WKUserScript alloc] initWithSource:self.javaScriptString
                                                          injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                                                       forMainFrameOnly:NO];
[webView.configuration.userContentController addUserScript:wkcookieScript];

WKUserScript vs evaluateJavaScript

WKWebView對於執行js程式碼提供了兩種方式,通過userContent新增一個WKUserScript物件的方式,以及通過webViewevaluateJavaScript:completionHandler:方式,注入js程式碼。

NSString *removeChildNode = @""
"var header = document.getElementsByTagName:('header')[0];"
"header.parentNote.removeChild(header);"
[self.webView evaluateJavaScript:removeChildNode completionHandler:nil];

首先要說明的是,這兩種方式都可以注入js程式碼,但是其內部的實現方式我沒有深入研究,WebKit核心是開源的,有興趣的同學可以看看。但是這兩種方式還是有一些功能上的區別的,可以根據具體業務場景去選擇對應的API

先說說evaluateJavaScript:completionHandler:的方式,這種方式一般是在頁面展示完成後執行的操作,用來呼叫js的函式並獲取返回值非常方便。當然也可以用來注入一段js程式碼,但需要自己控制注入時機。

WKUserScript則可以控制注入時機,可以針對document是否載入完選擇注入js。以及被注入的js是在當前頁面有效,還是包括其子頁面也有效。相對於evaluateJavaScript:方法,此方法不能獲得js執行後的返回值,所以兩個方法在功能上還是有區別的。

容器設計

設計思路

專案中一般不會直接使用WKWebView,而是通過對其進行一層包裝,成為一個WKWebViewController交給業務層使用。設計webViewVC時應該遵循簡單靈活的思想去設計,自身只提供展示功能,不涉及任何業務邏輯。對外提供展示導航欄、設定標題、進度條等功能,都可以通過WKWebViewConfiguration賦值並在WKWebViewController例項化的時候傳入。

對呼叫方提供js互動、webView生命週期、載入錯誤等回撥,外接通過對應的回撥進行處理。這些回撥都是可選的,不實現對webView載入也沒有影響。下面是例項程式碼,也可以把不同型別的回撥拆分定義不同的代理。

@protocol WKWebViewControllerDelegate <NSObject>
@optional
- (void)webViewDidStartLoad:(WKWebViewController *)webViewVC;
- (void)webViewDidFinishLoad:(WKWebViewController *)webViewVC;
- (void)webView:(WKWebViewController *)webViewVC didFailLoadWithError:(NSError *)error;
- (void)webview:(WKWebViewController *)webViewVC closeWeb:(NSString *)info;
- (void)webview:(WKWebViewController *)webViewVC login:(NSDictionary *)info;
- (void)webview:(WKWebViewController *)webViewVC jsCallbackParams:(NSDictionary *)params;
@end

此外,WKWebViewController還應該負責處理公共引數,並且可以基於公共引數進行擴充套件。這裡我們定義了一個方法,可以指定基礎引數的位置,是通過URL拼接、headerjs注入等方式新增,這個列舉是多選的,也就是可以在多個位置進行注入。除了基礎引數,還可以額外新增自定義引數,也會新增到指定的位置。

- (void)injectionParamsType:(SVParamsType)type additionalParams:(NSDictionary *)additionalParams;

複用池

WKWebView第一次初始化的時候,會先啟動webKit核心,並且有一些初始化操作,這個操作是非常消耗效能的。所以,複用池設計的第一步,是在App啟動的時候,初始化一個全域性的WKWebView

並且,建立兩個池子,建立visiblePool存放正在使用的,建立reusablePool存放空閒狀態的。並且,在頁面退出時,從visiblePool放入reusablePool的同時,應該將頁面進行回收,清除頁面上的資料。

當需要初始化一個webView容器時,從reusablePool中取出一個容器,並且放入到visiblePool中。通過複用池的實現,可以減少從初始化一個webView容器,到頁面展示出來的時間。

WKProcessPool

WKWebView中定義了processPool屬性,可以指定對應的程式池物件。每個webView都有自己的內容程式,如果不指定則預設是一個新的內容程式。內容程式中包括一些本地cookie、資源之類的,如果不在一個內容程式中,則不能共享這些資料。

可以建立一個公共的WKProcessPool,是一個單例物件。所有webView建立的時候,都使用同一個內容程式,即可實現資源共享。

UserAgent

User-Agent是在http協議中的一個請求頭欄位,用來告知伺服器一些資訊的,User-Agent中包含了很多欄位,例如系統版本、瀏覽器核心版本、網路環境等。這個欄位可以直接用系統提供的,也可以在原有User-Agent的基礎上新增其他欄位。

例如下面是從系統的webView中獲取到的User-Agent

Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Mobile/14F89

iOS9之後提供了customUserAgent屬性,直接為WKWebView設定User-Agent,而iOS9之前需要通過js寫入的方式對H5注入User-Agent

通訊協議

一個設計的比較好的WebView容器,應該具備很好的相互通訊功能,並且靈活具有擴充套件性。H5和客戶端的通訊主要有以下幾種場景。

  • js呼叫客戶端,以及js呼叫客戶端後獲取客戶端的callback回撥及引數。
  • 客戶端呼叫js,以及呼叫js後的callback回撥及引數。
  • 客戶端主動通知H5,客戶端的一些生命週期變化。例如進入鎖屏和進入前臺等系統生命週期。

js呼叫客戶端為例,有兩個緯度的呼叫。可以通過URLRouter的方式直接呼叫某個模組,這種呼叫方式遵循客戶端的URL定義即可調起,並且支援傳參。還可以通過userContentController的方式,進行頁面級的呼叫,例如關閉webView、調起登入功能等,也就是通過js呼叫客戶端的某個功能,這種方式需要客戶端提供對應的處理程式碼。

二者之間相互呼叫,儘量避免高頻呼叫,而且一般也不會有高頻呼叫的需求。但如果發生相同功能高頻呼叫,則需要設定一個actionID來區分不同的呼叫,以保證發生回撥時可以正常被區分。

callback的回撥方法也可以通過引數傳遞過來,這種方式靈活性比較強,如果固定寫死會有版本限制,較早版本的客戶端可能並不支援這個回撥。

處理回撥

webView的回撥除了基礎的呼叫,例如refresh重新整理當前頁面、close關閉當前頁面等,直接由對應的功能類來處理呼叫,其他的時間應該交給外界處理。

這裡的設計方案並不是一個事件對應一個回撥方法,然後外界遵循代理並實現多個代理方法的方式來實現。而是將每次回撥事件都封裝成一個物件,直接將這個物件回撥給外界處理,這樣靈活性更強一些,而且外界獲取的資訊也更多。事件模型的定義可以參考下面的。

@interface WKWebViewCallbackModel : NSObject
@property(nonatomic, strong) WKWebViewController *webViewVC;
@property(nonatomic, strong) WKCallType *type;
@property(nonatomic, copy) NSDictionary *parameters;
@property(nonatomic, copy) NSString *callbackID;
@property(nonatomic, copy) NSString *callbackFunction;
@end

持久化

目前H5頁面的持久化方案,主要是WebKit自帶的localStorageCookie,但是Cookie並不是用來做持久化操作的,所以也不應該給H5用來做持久化。如果想更穩定的進行持久化,可以考慮提供一個js bridgeCRUD介面,讓H5可以用來儲存和查詢資料。

持久化方案就採取和客戶端一致的方案,給H5單獨建一張資料表即可。

快取機制

快取規則

前端瀏覽器包括WKWebView在內,為了保證快速開啟頁面,減少使用者流量消耗,都會對資源進行快取。這個快取規則在WKWebView中也可以指定,如果我們為了保證每次的資原始檔都是最新的,也可以選擇不使用快取,但我們一般不這麼做。

  • NSURLRequestUseProtocolCachePolicy = 0,預設快取策略,和Safari核心的快取表現一樣。
  • NSURLRequestReloadIgnoringLocalCacheData = 1, 忽略本地快取,直接從伺服器獲取資料。
  • NSURLRequestReturnCacheDataElseLoad = 2, 本地有快取則使用快取,否則載入服務端資料。這種策略不會驗證快取是否過期。
  • NSURLRequestReturnCacheDataDontLoad = 3, 只從本地獲取,並且不判斷有效性和是否改變,本地沒有不會請求伺服器資料,請求會失敗。
  • NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, 忽略本地以及路由過程中的快取,從伺服器獲取最新資料。
  • NSURLRequestReloadRevalidatingCacheData = 5, 從服務端驗證快取是否可用,本地不可用則請求服務端資料。
  • NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,

15993877718102.jpg

根據蘋果預設的快取策略,會進行三步檢查。

  1. 快取是否存在。
  2. 驗證快取是否過期。
  3. 快取是否發生改變。

快取檔案

iOS9蘋果提供了快取管理類WKWebsiteDataStore,通過此類可以對磁碟上,指定型別的快取檔案進行查詢和刪除。因為現在很多App都從iOS9開始支援,所以非常推薦此API來管理本地快取,以及cookie。本地的檔案快取型別定義為以下幾種,常用的主要是cookiediskCachememoryCache這些。

  • WKWebsiteDataTypeFetchCache,磁碟中的快取,根據原始碼可以看出,型別是DOMCache
  • WKWebsiteDataTypeDiskCache,本地磁碟快取,和fetchCache的實現不同,是所有的快取資料
  • WKWebsiteDataTypeMemoryCache,本地記憶體快取
  • WKWebsiteDataTypeOfflineWebApplicationCache,離線web應用程式快取
  • WKWebsiteDataTypeCookiescookie快取
  • WKWebsiteDataTypeSessionStoragehtml會話儲存
  • WKWebsiteDataTypeLocalStoragehtml本地資料快取
  • WKWebsiteDataTypeWebSQLDatabasesWebSQL資料庫資料
  • WKWebsiteDataTypeIndexedDBDatabases,資料庫索引
  • WKWebsiteDataTypeServiceWorkerRegistrations,伺服器註冊資料

通過下面的方法可以獲取本地所有的快取檔案型別,返回的集合字串,就是上面定義的型別。

+ (NSSet<NSString *> *)allWebsiteDataTypes;

可以指定刪除某個時間段內,指定型別的資料,刪除後會回撥block

- (void)removeDataOfTypes:(NSSet<NSString *> *)dataTypes modifiedSince:(NSDate *)date completionHandler:(void (^)(void))completionHandler;

系統還提供了定製化更強的方法,通過fetchDataRecordsOfTypes:方法獲取指定型別的所有WKWebsiteDataRecord物件,此物件包含域名和型別兩個引數。可以根據域名和型別進行判斷,隨後呼叫removeDataOfTypes:方法傳入需要刪除的物件,對指定域名下的資料進行刪除。

// 獲取
- (void)fetchDataRecordsOfTypes:(NSSet<NSString *> *)dataTypes completionHandler:(void (^)(NSArray<WKWebsiteDataRecord *> *))completionHandler;
// 刪除
- (void)removeDataOfTypes:(NSSet<NSString *> *)dataTypes forDataRecords:(NSArray<WKWebsiteDataRecord *> *)dataRecords completionHandler:(void (^)(void))completionHandler;

http快取策略

客戶端和H5在打交道的時候,經常會出現頁面快取的問題,H5的開發同學就經常說“你清一下快取試試”,實際上發生這個問題的原因,在於H5的快取管理策略有問題。這裡就講一下H5的快取管理策略。

H5的快取管理其實就是利用http協議的欄位進行管理的,比較常用的是Cache-ControlLast-Modified搭配使用的方式。

  • Cache-Control:檔案快取有效時長,例如請求檔案後伺服器響應頭返回Cache-Control:max-age=600,則表示檔案有效時長600秒。所以此檔案在有效時長內,都不會發出網路請求,直到過期為止。
  • Last-Modified:請求檔案後伺服器響應頭中返回的,表示檔案的最新更新時間。如果Cache-Control過期後,則會請求伺服器並將這個時間放在請求頭的If-Modified-Since欄位中,伺服器收到請求後會進行時間對比,如果時間沒有發生改變則返回304,否則返回新的檔案和響應頭欄位,並返回200

Cache-Controlhttp1.1出來的,表示檔案的相對有效時長,在此之前還有Expires欄位,表示檔案的絕對有效時長,例如Expires: Thu, 10 Nov 2015 08:45:11 GMT,二者都可以用。

Last-Modified也有類似的欄位Etag,區別在於Last-Modified是以時間做對比,Etag是以檔案的雜湊值做對比。當檔案有效時長過期後,請求伺服器會在請求頭的If-None-Match欄位帶上Etag的值,並交由伺服器對比。

Cookie處理

眾所周知,http協議中是支援cookie設定的,伺服器可以通過Set-Cookie:欄位對瀏覽器設定cookie,並且還可以指定過期時間、域名等。這些在Chrome這些瀏覽器中比較適用,但是如果在客戶端內進行顯示,就需要客戶端傳一些引數過去,可以讓H5獲取到登入等狀態。

蘋果雖然提供了一些Cookie管理的API,但在WKWebView的使用上還是有很多坑的,最後我會給出一個比較通用的方案。

WKWebView Cookie設計

之前使用UIWebView的時候,和傳統的cookie管理類NSHTTPCookieStorage讀取的是一塊區域,或者說UIWebViewcookie也是由此類管理的。但是WKWebViewcookie設計不太一樣,和Appcookie並沒有儲存在同一塊記憶體區域,所以二者需要分開做處理。

WKWebViewcookieNSHTTPCookieStorage之間也有同步操作,但是這個同步有明顯的延時,而且規則不容易琢磨。所以為了程式碼的穩定性,還是自己處理cookie比較合適。

WKapp是兩個程式,cookie也是兩份,但是WKcookieapp的沙盒裡。有一個定時同步,但是並沒有一個特定規則,所以最好不要依賴同步。WKcookie變化只有兩個時機,一個是js執行程式碼setCookie,另一個是response返回cookie

WKWebsiteDataStore

Cookie的管理一直都是WKWebView的一個弊端,對於Cookie的處理很不方便。在iOS9中可以通過WKWebsiteDataStoreCookie進行管理,但是用起來並不直觀,需要進行dataType進行篩選並刪除。而且WKWebsiteDataStore自身功能並不具備新增功能,所以對cookie的處理也只有刪除,不能新增cookie

if (@available(iOS 9.0, *)) {
    NSSet *cookieTypeSet = [NSSet setWithObject:WKWebsiteDataTypeCookies];
    [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:cookieTypeSet modifiedSince:[NSDate dateWithTimeIntervalSince1970:0] completionHandler:^{
        
    }];
}

WKHTTPCookieStore

iOS11中蘋果在WKWebsiteDataStore的基礎上,為其增加了WKHTTPCookieStore類專門進行cookie的處理,並且支援增加、刪除、查詢三種操作,還可以註冊一個observercookie的變化進行監聽,當cookie發生變化後通過回撥的方法通知監聽者。

WKWebsiteDataStore可以獲取H5頁面通過document.cookie的方式寫入的cookie,以及伺服器通過Set-Cookie的方式寫入的cookie,所以還是很推薦使用這個類來管理cookie的,可惜只支援iOS11

下面是給WKWebView新增cookie的一段程式碼。

NSMutableDictionary *params = [NSMutableDictionary dictionary];
[params setObject:@"password" forKey:NSHTTPCookieName];
[params setObject:@"e10adc3949ba5" forKey:NSHTTPCookieValue];
[params setObject:@"www.google.com" forKey:NSHTTPCookieDomain];
[params setObject:@"/" forKey:NSHTTPCookiePath];
[params setValue:[NSDate dateWithTimeIntervalSinceNow:60*60*72] forKey:NSHTTPCookieExpires];
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:params];
[self.cookieWebview.configuration.websiteDataStore.httpCookieStore setCookie:cookie completionHandler:nil];

我公司方案

處理Cookie最好的方式是通過WKHTTPCookieStore來處理,但其只支援iOS11及以上裝置,所以這種方案目前還不能作為我們的選擇。其次是WKWebsiteDataStore,但其只能作為一個刪除cookie的使用,並不不能用來管理cookie

我公司的方案是,通過iOS8推出的WKUserContentController來管理webViewcookie,通過NSHTTPCookieStorage來管理網路請求的cookie,例如H5發出的請求。通過NSURLSessionNSURLConnection發出的請求,都會預設帶上NSHTTPCookieStorage中的cookieH5內部的請求也會被系統交給NSURLSession處理。

在程式碼實現層面,監聽didFinishLaunching通知,在程式啟動時從服務端請求使用者相關資訊,當然從本地取也可以,都是一樣的。資料是keyvalue的形式下發,按照key=value的形式拼接,並通過document.cookie組裝成設定cookiejs程式碼,所有程式碼拼接為一個以分號分割的字串,後面給webViewcookie時就通過這個字串執行。

對於網路請求的cookie,通過NSHTTPCookieStorage直接將cookie種到根域名下的,可以對根域名下所有子域名生效,這裡的處理比較簡單。

SVREQUEST.type(SVRequestTypePost).parameters(params).success(^(NSDictionary *cookieDict) {
    self.cookieData = [cookieDict as:[NSDictionary class]];
    [self addCookieWithDict:cookieDict forHost:@".google.com"];
    [self addCookieWithDict:cookieDict forHost:@".google.cn"];
    [self addCookieWithDict:cookieDict forHost:@".google.jp"];
    
    NSMutableString *scriptString = [NSMutableString string];
    for (NSString *key in self.cookieData.allKeys) {
        NSString *cookieString = [NSString stringWithFormat:@"%@=%@", key, cookieDict[key]];
        [scriptString appendString:[NSString stringWithFormat:@"document.cookie = '%@;expires=Fri, 31 Dec 9999 23:59:59 GMT;';", cookieString]];
    }
    self.webviewCookie = scriptString;
}).startRequest();

- (void)addCookieWithDict:(NSDictionary *)dict forHost:(NSString *)host {
    [dict enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull value, BOOL * _Nonnull stop) {
        NSMutableDictionary *properties = [NSMutableDictionary dictionary];
        [properties setObject:key forKey:NSHTTPCookieName];
        [properties setObject:value forKey:NSHTTPCookieValue];
        [properties setObject:host forKey:NSHTTPCookieDomain];
        [properties setObject:@"/" forKey:NSHTTPCookiePath];
        [properties setValue:[NSDate dateWithTimeIntervalSinceNow:60*60*72] forKey:NSHTTPCookieExpires];
        NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:properties];
        [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
    }];
}

webViewcookie是通過WKUserContentController寫入js的方式實現的,也就是上面拼接的js字串。但是這個類有一個問題就是不能持久化cookie,也就是cookieuserContentController的宣告週期,如果退出Appcookie就會消失,下次進入App還需要種一次,這是個大問題。

所以我司的處理方式是在decidePolicyForNavigationAction:回撥方法中加入下面這段程式碼,程式碼中會判斷此域名是否種過cookie,如果沒有則種cookie。對於cookie的處理,我新建了一個cookieWebview專門處理cookie的問題,當執行addUserScript後,通過loadHTMLString:baseURL:載入一個空的本地html,並將域名設定為當前將要顯示頁面的域名,從而使剛才種的cookie對當前processPool內所有的webView生效。

這種方案種cookie是同步執行的,而且對webView的影響很小,經過我的測試,平均新增一次cookie只需要消耗28ms的時間。從使用者的角度來看是無感知的,並不會有頁面的卡頓或重新重新整理。

- (void)setCookieWithUrl:(NSURL *)url {
    NSString *host = [url host];
    if ([self.cookieURLs containsObject:host]) {
        return;
    }
    [self.cookieURLs addObject:host];
    
    WKUserScript *wkcookieScript = [[WKUserScript alloc] initWithSource:self.webviewCookie
                                                          injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                                                       forMainFrameOnly:NO];
    [self.cookieWebview.configuration.userContentController addUserScript:wkcookieScript];
    
    NSString *baseWebUrl = [NSString stringWithFormat:@"%@://%@", url.scheme, url.host];
    [self.cookieWebview loadHTMLString:@"" baseURL:[NSURL URLWithString:baseWebUrl]];
}

刪除cookie的處理則相對比較簡單,NSHTTPCookieStorage通過cookies屬性遍歷到自己需要刪除的NSHTTPCookie,呼叫方法將其刪除即可。webView的刪除方法更是簡單粗暴,直接呼叫removeAllUserScripts刪除所有WKUserScript即可。

- (void)removeWKWebviewCookie {
    self.webviewCookie = nil;
    [self.cookieWebview.configuration.userContentController removeAllUserScripts];
    
    NSMutableArray<NSHTTPCookie *> *cookies = [NSMutableArray array];
    [[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull cookie, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([self.cookieData.allKeys containsObject:cookie.name]) {
            [cookies addObjectOrNil:cookie];
        }
    }];
    
    [cookies enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull cookie, NSUInteger idx, BOOL * _Nonnull stop) {
        [[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:cookie];
    }];
}

白屏問題

如果WKWebView載入記憶體佔用過多的頁面,會導致WebContent Process程式崩潰,進而頁面出現白屏,也有可能是系統其他程式佔用記憶體過多導致的白屏。對於低記憶體導致的白屏問題,有以下兩種方案可以解決。

iOS9中蘋果推出了下面的API,當WebContent程式發生異常退出時,會回撥此API。可以在這個API中進行對應的處理,例如展示一個異常頁面。

- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView;

如果從其他App回來導致白屏問題,可以在檢視將要顯示的時候,判斷webView.title是否為空。如果為空則展示異常頁面。

相關文章