概述
之前主要使用UIWebView
進行頁面的載入,但是UIWebView
存在很多問題,在2020年已經被蘋果正式拋棄。所以本篇文章主要講解WKWebView
,WKWebView
從iOS8
開始支援,現在大多數App
應該都不支援iOS7
了。
UIWebView
存在兩個問題,一個是記憶體消耗比較大,另一個是效能很差。WKWebView
相對於UIWebView
來說,效能要比UIWebView
效能要好太多,重新整理率能達到60FPS
。記憶體佔用也比UIWebView
要小。
WKWebView
是一個多程式元件,Network
、UI Render
都在獨立的程式中完成。
由於WKWebView
和App
不在同一個程式,如果WKWebView
程式崩潰並不會導致應用崩潰,僅僅是頁面白屏等異常。頁面的載入、渲染等消耗記憶體和效能的操作,都在WKWebView
的程式中處理,處理後再將結果交給App
程式用於顯示,所以App
程式的效能消耗會小很多。
網頁載入流程
- 通過域名的方式請求伺服器,請求前瀏覽器會做一個
DNS
解析,並將IP
地址返回給瀏覽器。 - 瀏覽器使用
IP
地址請求伺服器,並且開始握手過程。TCP
是三次握手,如果使用https
則還需要進行TLS
的握手,握手後根據協議欄位選擇是否保持連線。 - 握手完成後,瀏覽器向服務端傳送請求,獲取
html
檔案。 - 伺服器解析請求,並由
CDN
伺服器返回對應的資原始檔。 - 瀏覽器收到伺服器返回的
html
檔案,交由html
解析器進行解析。 - 解析
html
由上到下進行解析xml
標籤,過程中如果遇到css
或資原始檔,都會進行非同步載入,遇到js
則會掛起當前html
解析任務,請求js
並返回後繼續解析。因為js
檔案可能會對DOM
樹進行修改。 - 解析完
html
,並執行完js
程式碼,形成最終的DOM
樹。通過DOM
配合css
檔案找出每個節點的最終展示樣式,並交由瀏覽器進行渲染展示 - 結束連結。
代理方法
WKWebView
和UIWebView
的代理方法發生了一些改變,WKWebView
的流程更加細化了。例如之前UI
結束請求後,會立刻渲染到webView
上。而WKWebView
則會在渲染到螢幕之前,會回撥一個代理方法,代理方法決定是否渲染到螢幕上。這樣就可以對請求下來的資料做一次校驗,防止資料被更改,或驗證檢視是否允許被顯示到螢幕上。
除此之外,WKWebView
相對於UIWebView
還多了一些定製化操作。
- 重定向的回撥,可以在請求重定向時獲取到這次操作。
- 當
WKWebView
程式異常退出時,可以通過回撥獲取。 - 自定義處理證照。
- 更深層的
UI
定製操作,將alert
等UI
操作交給原生層面處理,而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;
頁面載入及渲染完成,會呼叫此方法,呼叫此方法時H5
的dom
已經解析並渲染完成,展示在螢幕上。所以在此方法中可以進行一些載入完成的操作,例如移除進度條,重置網路指示器等。
- (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
物件的方式,以及通過webView
的evaluateJavaScript: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
拼接、header
、js
注入等方式新增,這個列舉是多選的,也就是可以在多個位置進行注入。除了基礎引數,還可以額外新增自定義引數,也會新增到指定的位置。
- (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
自帶的localStorage
和Cookie
,但是Cookie
並不是用來做持久化操作的,所以也不應該給H5
用來做持久化。如果想更穩定的進行持久化,可以考慮提供一個js bridge
的CRUD
介面,讓H5
可以用來儲存和查詢資料。
持久化方案就採取和客戶端一致的方案,給H5
單獨建一張資料表即可。
快取機制
快取規則
前端瀏覽器包括WKWebView
在內,為了保證快速開啟頁面,減少使用者流量消耗,都會對資源進行快取。這個快取規則在WKWebView
中也可以指定,如果我們為了保證每次的資原始檔都是最新的,也可以選擇不使用快取,但我們一般不這麼做。
NSURLRequestUseProtocolCachePolicy = 0
,預設快取策略,和Safari
核心的快取表現一樣。NSURLRequestReloadIgnoringLocalCacheData = 1,
忽略本地快取,直接從伺服器獲取資料。NSURLRequestReturnCacheDataElseLoad = 2
, 本地有快取則使用快取,否則載入服務端資料。這種策略不會驗證快取是否過期。NSURLRequestReturnCacheDataDontLoad = 3
, 只從本地獲取,並且不判斷有效性和是否改變,本地沒有不會請求伺服器資料,請求會失敗。NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4
, 忽略本地以及路由過程中的快取,從伺服器獲取最新資料。NSURLRequestReloadRevalidatingCacheData = 5
, 從服務端驗證快取是否可用,本地不可用則請求服務端資料。NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData
,
根據蘋果預設的快取策略,會進行三步檢查。
- 快取是否存在。
- 驗證快取是否過期。
- 快取是否發生改變。
快取檔案
iOS9
蘋果提供了快取管理類WKWebsiteDataStore
,通過此類可以對磁碟上,指定型別的快取檔案進行查詢和刪除。因為現在很多App
都從iOS9
開始支援,所以非常推薦此API
來管理本地快取,以及cookie
。本地的檔案快取型別定義為以下幾種,常用的主要是cookie
、diskCache
、memoryCache
這些。
WKWebsiteDataTypeFetchCache
,磁碟中的快取,根據原始碼可以看出,型別是DOMCache
WKWebsiteDataTypeDiskCache
,本地磁碟快取,和fetchCache
的實現不同,是所有的快取資料WKWebsiteDataTypeMemoryCache
,本地記憶體快取WKWebsiteDataTypeOfflineWebApplicationCache
,離線web
應用程式快取WKWebsiteDataTypeCookies
,cookie
快取WKWebsiteDataTypeSessionStorage
,html
會話儲存WKWebsiteDataTypeLocalStorage
,html
本地資料快取WKWebsiteDataTypeWebSQLDatabases
,WebSQL
資料庫資料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-Control
和Last-Modified
搭配使用的方式。
Cache-Control
:檔案快取有效時長,例如請求檔案後伺服器響應頭返回Cache-Control:max-age=600
,則表示檔案有效時長600
秒。所以此檔案在有效時長內,都不會發出網路請求,直到過期為止。Last-Modified
:請求檔案後伺服器響應頭中返回的,表示檔案的最新更新時間。如果Cache-Control
過期後,則會請求伺服器並將這個時間放在請求頭的If-Modified-Since
欄位中,伺服器收到請求後會進行時間對比,如果時間沒有發生改變則返回304
,否則返回新的檔案和響應頭欄位,並返回200
。
Cache-Control
是http1.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
讀取的是一塊區域,或者說UIWebView
的cookie
也是由此類管理的。但是WKWebView
的cookie
設計不太一樣,和App
的cookie
並沒有儲存在同一塊記憶體區域,所以二者需要分開做處理。
WKWebView
的cookie
和NSHTTPCookieStorage
之間也有同步操作,但是這個同步有明顯的延時,而且規則不容易琢磨。所以為了程式碼的穩定性,還是自己處理cookie
比較合適。
WK
和app
是兩個程式,cookie
也是兩份,但是WK
的cookie
在app
的沙盒裡。有一個定時同步,但是並沒有一個特定規則,所以最好不要依賴同步。WK
的cookie
變化只有兩個時機,一個是js
執行程式碼setCookie
,另一個是response
返回cookie
。
WKWebsiteDataStore
Cookie
的管理一直都是WKWebView
的一個弊端,對於Cookie
的處理很不方便。在iOS9
中可以通過WKWebsiteDataStore
對Cookie
進行管理,但是用起來並不直觀,需要進行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
的處理,並且支援增加、刪除、查詢三種操作,還可以註冊一個observer
對cookie
的變化進行監聽,當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
來管理webView
的cookie
,通過NSHTTPCookieStorage
來管理網路請求的cookie
,例如H5
發出的請求。通過NSURLSession
、NSURLConnection
發出的請求,都會預設帶上NSHTTPCookieStorage
中的cookie
,H5
內部的請求也會被系統交給NSURLSession
處理。
在程式碼實現層面,監聽didFinishLaunching
通知,在程式啟動時從服務端請求使用者相關資訊,當然從本地取也可以,都是一樣的。資料是key
、value
的形式下發,按照key=value
的形式拼接,並通過document.cookie
組裝成設定cookie
的js
程式碼,所有程式碼拼接為一個以分號分割的字串,後面給webView
種cookie
時就通過這個字串執行。
對於網路請求的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];
}];
}
對webView
種cookie
是通過WKUserContentController
寫入js
的方式實現的,也就是上面拼接的js
字串。但是這個類有一個問題就是不能持久化cookie
,也就是cookie
隨userContentController
的宣告週期,如果退出App
則cookie
就會消失,下次進入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
是否為空。如果為空則展示異常頁面。