探秘WKWebView

該文章屬于劉小壯原創(chuàng),轉(zhuǎn)載請注明:劉小壯


概述

之前主要使用UIWebView進行頁面的加載,但是UIWebView存在很多問題锻煌,在2020年已經(jīng)被蘋果正式拋棄。所以本篇文章主要講解WKWebView敛腌,WKWebViewiOS8開始支持祭饭,現(xiàn)在大多數(shù)App應(yīng)該都不支持iOS7了章蚣。

UIWebView存在兩個問題站欺,一個是內(nèi)存消耗比較大,另一個是性能很差纤垂。WKWebView相對于UIWebView來說矾策,性能要比UIWebView性能要好太多,刷新率能達到60FPS峭沦。內(nèi)存占用也比UIWebView要小贾虽。

WKWebView是一個多進程組件,Network吼鱼、UI Render都在獨立的進程中完成蓬豁。

由于WKWebViewApp不在同一個進程,如果WKWebView進程崩潰并不會導(dǎo)致應(yīng)用崩潰菇肃,僅僅是頁面白屏等異常地粪。頁面的載入、渲染等消耗內(nèi)存和性能的操作琐谤,都在WKWebView的進程中處理蟆技,處理后再將結(jié)果交給App進程用于顯示,所以App進程的性能消耗會小很多。

網(wǎng)頁加載流程

  1. 通過域名的方式請求服務(wù)器质礼,請求前瀏覽器會做一個DNS解析旺聚,并將IP地址返回給瀏覽器。
  2. 瀏覽器使用IP地址請求服務(wù)器眶蕉,并且開始握手過程砰粹。TCP是三次握手,如果使用https則還需要進行TLS的握手造挽,握手后根據(jù)協(xié)議字段選擇是否保持連接碱璃。
  3. 握手完成后,瀏覽器向服務(wù)端發(fā)送請求饭入,獲取html文件厘贼。
  4. 服務(wù)器解析請求,并由CDN服務(wù)器返回對應(yīng)的資源文件圣拄。
  5. 瀏覽器收到服務(wù)器返回的html文件,交由html解析器進行解析毁欣。
  6. 解析html由上到下進行解析xml標(biāo)簽庇谆,過程中如果遇到css或資源文件,都會進行異步加載凭疮,遇到js則會掛起當(dāng)前html解析任務(wù)饭耳,請求js并返回后繼續(xù)解析。因為js文件可能會對DOM樹進行修改执解。
  7. 解析完html寞肖,并執(zhí)行完js代碼,形成最終的DOM樹衰腌。通過DOM配合css文件找出每個節(jié)點的最終展示樣式新蟆,并交由瀏覽器進行渲染展示
  8. 結(jié)束鏈接。

代理方法


WKWebViewUIWebView的代理方法發(fā)生了一些改變右蕊,WKWebView的流程更加細化了琼稻。例如之前UI結(jié)束請求后,會立刻渲染到webView上饶囚。而WKWebView則會在渲染到屏幕之前帕翻,會回調(diào)一個代理方法,代理方法決定是否渲染到屏幕上萝风。這樣就可以對請求下來的數(shù)據(jù)做一次校驗嘀掸,防止數(shù)據(jù)被更改,或驗證視圖是否允許被顯示到屏幕上规惰。

除此之外睬塌,WKWebView相對于UIWebView還多了一些定制化操作。

  1. 重定向的回調(diào),可以在請求重定向時獲取到這次操作衫仑。
  2. 當(dāng)WKWebView進程異常退出時梨与,可以通過回調(diào)獲取叽讳。
  3. 自定義處理證書统台。
  4. 更深層的UI定制操作,將alertUI操作交給原生層面處理裕膀,而UI方案UIAlertView是直接webView顯示的瞄崇。

WKUIDelegate

WKWebView將很多UI的顯示都交給原生層面去處理呻粹,例如彈窗或者輸入框的顯示。這樣如果項目里有統(tǒng)一定義的彈窗苏研,就可以直接調(diào)用自定義彈窗等浊,而不是只能展示系統(tǒng)彈窗。

WKWebView中摹蘑,系統(tǒng)將彈窗的顯示交由客戶端來控制筹燕。客戶端可以通過下面的回調(diào)方法獲取到彈窗的顯示信息衅鹿,并由客戶端來調(diào)起UIAlertController來展示撒踪。參數(shù)中有一個completionHandler的回調(diào)block,需要客戶端一定要調(diào)用大渤,如果不調(diào)用則會發(fā)生崩潰制妄。

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

有時候H5會要求用戶進行一些輸入,例如用戶名密碼之類的泵三「蹋客戶端可以通過下面的方法獲取到輸入框事件,并由客戶端展示輸入框烫幕,用戶輸入完成后將結(jié)果回調(diào)給completionHandler中俺抽。

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

WKNavigationDelegate

關(guān)于加載流程相關(guān)的方法,都被抽象到WKNavigationDelegate中纬霞,這里挑幾個比較常用的方法講一下凌埂。

下面的方法,通過decisionHandler回調(diào)中返回一個枚舉類型的參數(shù)诗芜,表示是否允許頁面加載瞳抓。這里可以對域名進行判斷,如果是站外域名伏恐,則可以提示用戶是否進行跳轉(zhuǎn)孩哑。如果是跳轉(zhuǎn)其他App或商店的URL,則可以通過openURL進行跳轉(zhuǎn)翠桦,并將這次請求攔截横蜒。包括cookie的處理也在此方法中完成胳蛮,后面會詳細講到cookie的處理。

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

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

開始加載頁面谋逻,并請求服務(wù)器呆馁。

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

當(dāng)頁面加載失敗的時候,會回調(diào)此方法毁兆,包括timeout等錯誤浙滤。在這個頁面可以展示錯誤頁面,清空進度條气堕,重置網(wǎng)絡(luò)指示器等操作纺腊。需要注意的是,調(diào)用goBack時也會執(zhí)行此方法茎芭,可以通過error的狀態(tài)判斷是否NSURLErrorCancelled來過濾掉摹菠。

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

頁面加載及渲染完成,會調(diào)用此方法骗爆,調(diào)用此方法時H5dom已經(jīng)解析并渲染完成,展示在屏幕上蔽介。所以在此方法中可以進行一些加載完成的操作摘投,例如移除進度條,重置網(wǎng)絡(luò)指示器等虹蓄。

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

WKUserContentController


回調(diào)

WKWebView將和js的交互都由WKUserContentController類來處理犀呼,后面統(tǒng)稱為userContent

如果需要接收并處理js的調(diào)用薇组,通過調(diào)用addScriptMessageHandler:name:方法外臂,并傳入一個實現(xiàn)了WKScriptMessageHandler協(xié)議的對象,即可接收js的回調(diào)律胀,由于userContent會強引用傳入的對象宋光,所以應(yīng)該是新創(chuàng)建一個對象,而不是self炭菌。注冊對象時罪佳,后面的name就是js調(diào)用的函數(shù)名。

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

dealloc中應(yīng)該通過下面的方法黑低,移除對指定name的處理赘艳。

[userContent removeScriptMessageHandlerForName:@"clientCallback"];

H5通過下面的代碼即可對客戶端發(fā)起調(diào)用,調(diào)用是通過postMessage函數(shù)傳一個json串過來,需要加上轉(zhuǎn)移字符蕾管〖咸ぃ客戶端接收到調(diào)用后,根據(jù)回調(diào)方法傳入的WKScriptMessage對象掰曾,獲取到body字典旭蠕,解析傳入的參數(shù)即可。

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

調(diào)用

原生調(diào)用H5的方法也是一樣婴梧,創(chuàng)建一個WKUserScript對象下梢,并將js代碼當(dāng)做參數(shù)傳入。除了調(diào)用js代碼塞蹭,也可以通過此方法注入代碼改變頁面dom孽江,但是這樣代碼量較大,不建議這么做番电。

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

WKUserScript vs evaluateJavaScript

WKWebView對于執(zhí)行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代碼娩井,但是其內(nèi)部的實現(xiàn)方式我沒有深入研究暇屋,WebKit內(nèi)核是開源的,有興趣的同學(xué)可以看看洞辣。但是這兩種方式還是有一些功能上的區(qū)別的咐刨,可以根據(jù)具體業(yè)務(wù)場景去選擇對應(yīng)的API

先說說evaluateJavaScript:completionHandler:的方式扬霜,這種方式一般是在頁面展示完成后執(zhí)行的操作定鸟,用來調(diào)用js的函數(shù)并獲取返回值非常方便。當(dāng)然也可以用來注入一段js代碼著瓶,但需要自己控制注入時機联予。

WKUserScript則可以控制注入時機,可以針對document是否加載完選擇注入js材原。以及被注入的js是在當(dāng)前頁面有效沸久,還是包括其子頁面也有效。相對于evaluateJavaScript:方法余蟹,此方法不能獲得js執(zhí)行后的返回值麦向,所以兩個方法在功能上還是有區(qū)別的。

容器設(shè)計


設(shè)計思路

項目中一般不會直接使用WKWebView客叉,而是通過對其進行一層包裝诵竭,成為一個WKWebViewController交給業(yè)務(wù)層使用话告。設(shè)計webViewVC時應(yīng)該遵循簡單靈活的思想去設(shè)計,自身只提供展示功能卵慰,不涉及任何業(yè)務(wù)邏輯沙郭。對外提供展示導(dǎo)航欄、設(shè)置標(biāo)題裳朋、進度條等功能病线,都可以通過WKWebViewConfiguration賦值并在WKWebViewController實例化的時候傳入。

對調(diào)用方提供js交互鲤嫡、webView生命周期送挑、加載錯誤等回調(diào),外接通過對應(yīng)的回調(diào)進行處理暖眼。這些回調(diào)都是可選的惕耕,不實現(xiàn)對webView加載也沒有影響。下面是實例代碼诫肠,也可以把不同類型的回調(diào)拆分定義不同的代理司澎。

@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還應(yīng)該負責(zé)處理公共參數(shù)栋豫,并且可以基于公共參數(shù)進行擴展挤安。這里我們定義了一個方法,可以指定基礎(chǔ)參數(shù)的位置丧鸯,是通過URL拼接蛤铜、headerjs注入等方式添加丛肢,這個枚舉是多選的昂羡,也就是可以在多個位置進行注入。除了基礎(chǔ)參數(shù)摔踱,還可以額外添加自定義參數(shù),也會添加到指定的位置怨愤。

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

復(fù)用池

WKWebView第一次初始化的時候派敷,會先啟動webKit內(nèi)核,并且有一些初始化操作撰洗,這個操作是非常消耗性能的篮愉。所以,復(fù)用池設(shè)計的第一步差导,是在App啟動的時候试躏,初始化一個全局的WKWebView

并且设褐,創(chuàng)建兩個池子颠蕴,創(chuàng)建visiblePool存放正在使用的泣刹,創(chuàng)建reusablePool存放空閑狀態(tài)的。并且犀被,在頁面退出時椅您,從visiblePool放入reusablePool的同時,應(yīng)該將頁面進行回收寡键,清除頁面上的數(shù)據(jù)掀泳。

當(dāng)需要初始化一個webView容器時,從reusablePool中取出一個容器西轩,并且放入到visiblePool中员舵。通過復(fù)用池的實現(xiàn),可以減少從初始化一個webView容器藕畔,到頁面展示出來的時間马僻。

WKProcessPool

WKWebView中定義了processPool屬性,可以指定對應(yīng)的進程池對象劫流。每個webView都有自己的內(nèi)容進程巫玻,如果不指定則默認是一個新的內(nèi)容進程。內(nèi)容進程中包括一些本地cookie祠汇、資源之類的仍秤,如果不在一個內(nèi)容進程中,則不能共享這些數(shù)據(jù)可很。

可以創(chuàng)建一個公共的WKProcessPool诗力,是一個單例對象。所有webView創(chuàng)建的時候我抠,都使用同一個內(nèi)容進程苇本,即可實現(xiàn)資源共享。

UserAgent

User-Agent是在http協(xié)議中的一個請求頭字段菜拓,用來告知服務(wù)器一些信息的瓣窄,User-Agent中包含了很多字段,例如系統(tǒng)版本纳鼎、瀏覽器內(nèi)核版本俺夕、網(wǎng)絡(luò)環(huán)境等。這個字段可以直接用系統(tǒng)提供的贱鄙,也可以在原有User-Agent的基礎(chǔ)上添加其他字段劝贸。

例如下面是從系統(tǒng)的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設(shè)置User-Agent映九,而iOS9之前需要通過js寫入的方式對H5注入User-Agent

通信協(xié)議

一個設(shè)計的比較好的WebView容器瞎颗,應(yīng)該具備很好的相互通信功能件甥,并且靈活具有擴展性捌议。H5和客戶端的通信主要有以下幾種場景。

  • js調(diào)用客戶端嚼蚀,以及js調(diào)用客戶端后獲取客戶端的callback回調(diào)及參數(shù)禁灼。
  • 客戶端調(diào)用js,以及調(diào)用js后的callback回調(diào)及參數(shù)轿曙。
  • 客戶端主動通知H5弄捕,客戶端的一些生命周期變化。例如進入鎖屏和進入前臺等系統(tǒng)生命周期导帝。

js調(diào)用客戶端為例守谓,有兩個緯度的調(diào)用∧ィ可以通過URLRouter的方式直接調(diào)用某個模塊斋荞,這種調(diào)用方式遵循客戶端的URL定義即可調(diào)起,并且支持傳參虐秦。還可以通過userContentController的方式平酿,進行頁面級的調(diào)用,例如關(guān)閉webView悦陋、調(diào)起登錄功能等蜈彼,也就是通過js調(diào)用客戶端的某個功能,這種方式需要客戶端提供對應(yīng)的處理代碼俺驶。

二者之間相互調(diào)用幸逆,盡量避免高頻調(diào)用,而且一般也不會有高頻調(diào)用的需求暮现。但如果發(fā)生相同功能高頻調(diào)用还绘,則需要設(shè)置一個actionID來區(qū)分不同的調(diào)用,以保證發(fā)生回調(diào)時可以正常被區(qū)分栖袋。

callback的回調(diào)方法也可以通過參數(shù)傳遞過來拍顷,這種方式靈活性比較強,如果固定寫死會有版本限制塘幅,較早版本的客戶端可能并不支持這個回調(diào)昔案。

處理回調(diào)

webView的回調(diào)除了基礎(chǔ)的調(diào)用,例如refresh刷新當(dāng)前頁面晌块、close關(guān)閉當(dāng)前頁面等,直接由對應(yīng)的功能類來處理調(diào)用帅霜,其他的時間應(yīng)該交給外界處理匆背。

這里的設(shè)計方案并不是一個事件對應(yīng)一個回調(diào)方法,然后外界遵循代理并實現(xiàn)多個代理方法的方式來實現(xiàn)身冀。而是將每次回調(diào)事件都封裝成一個對象钝尸,直接將這個對象回調(diào)給外界處理括享,這樣靈活性更強一些,而且外界獲取的信息也更多珍促。事件模型的定義可以參考下面的铃辖。

@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并不是用來做持久化操作的娇斩,所以也不應(yīng)該給H5用來做持久化。如果想更穩(wěn)定的進行持久化穴翩,可以考慮提供一個js bridgeCRUD接口犬第,讓H5可以用來存儲和查詢數(shù)據(jù)。

持久化方案就采取和客戶端一致的方案芒帕,給H5單獨建一張數(shù)據(jù)表即可歉嗓。

緩存機制


緩存規(guī)則

前端瀏覽器包括WKWebView在內(nèi),為了保證快速打開頁面背蟆,減少用戶流量消耗鉴分,都會對資源進行緩存。這個緩存規(guī)則在WKWebView中也可以指定带膀,如果我們?yōu)榱吮WC每次的資源文件都是最新的志珍,也可以選擇不使用緩存,但我們一般不這么做本砰。

  • NSURLRequestUseProtocolCachePolicy = 0碴裙,默認緩存策略,和Safari內(nèi)核的緩存表現(xiàn)一樣点额。
  • NSURLRequestReloadIgnoringLocalCacheData = 1, 忽略本地緩存舔株,直接從服務(wù)器獲取數(shù)據(jù)。
  • NSURLRequestReturnCacheDataElseLoad = 2, 本地有緩存則使用緩存还棱,否則加載服務(wù)端數(shù)據(jù)载慈。這種策略不會驗證緩存是否過期。
  • NSURLRequestReturnCacheDataDontLoad = 3, 只從本地獲取珍手,并且不判斷有效性和是否改變办铡,本地沒有不會請求服務(wù)器數(shù)據(jù),請求會失敗琳要。
  • NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, 忽略本地以及路由過程中的緩存寡具,從服務(wù)器獲取最新數(shù)據(jù)。
  • NSURLRequestReloadRevalidatingCacheData = 5, 從服務(wù)端驗證緩存是否可用稚补,本地不可用則請求服務(wù)端數(shù)據(jù)童叠。
  • NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,
檢查緩存

根據(jù)蘋果默認的緩存策略,會進行三步檢查课幕。

  1. 緩存是否存在厦坛。
  2. 驗證緩存是否過期五垮。
  3. 緩存是否發(fā)生改變。

緩存文件

iOS9蘋果提供了緩存管理類WKWebsiteDataStore杜秸,通過此類可以對磁盤上放仗,指定類型的緩存文件進行查詢和刪除。因為現(xiàn)在很多App都從iOS9開始支持撬碟,所以非常推薦此API來管理本地緩存诞挨,以及cookie。本地的文件緩存類型定義為以下幾種小作,常用的主要是cookie亭姥、diskCachememoryCache這些顾稀。

  • WKWebsiteDataTypeFetchCache达罗,磁盤中的緩存,根據(jù)源碼可以看出静秆,類型是DOMCache
  • WKWebsiteDataTypeDiskCache粮揉,本地磁盤緩存,和fetchCache的實現(xiàn)不同抚笔,是所有的緩存數(shù)據(jù)
  • WKWebsiteDataTypeMemoryCache扶认,本地內(nèi)存緩存
  • WKWebsiteDataTypeOfflineWebApplicationCache,離線web應(yīng)用程序緩存
  • WKWebsiteDataTypeCookies殊橙,cookie緩存
  • WKWebsiteDataTypeSessionStorage辐宾,html會話存儲
  • WKWebsiteDataTypeLocalStoragehtml本地數(shù)據(jù)緩存
  • WKWebsiteDataTypeWebSQLDatabases膨蛮,WebSQL數(shù)據(jù)庫數(shù)據(jù)
  • WKWebsiteDataTypeIndexedDBDatabases叠纹,數(shù)據(jù)庫索引
  • WKWebsiteDataTypeServiceWorkerRegistrations,服務(wù)器注冊數(shù)據(jù)

通過下面的方法可以獲取本地所有的緩存文件類型敞葛,返回的集合字符串誉察,就是上面定義的類型。

+ (NSSet<NSString *> *)allWebsiteDataTypes;

可以指定刪除某個時間段內(nèi)惹谐,指定類型的數(shù)據(jù)持偏,刪除后會回調(diào)block

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

系統(tǒng)還提供了定制化更強的方法氨肌,通過fetchDataRecordsOfTypes:方法獲取指定類型的所有WKWebsiteDataRecord對象鸿秆,此對象包含域名和類型兩個參數(shù)≡跚簦可以根據(jù)域名和類型進行判斷卿叽,隨后調(diào)用removeDataOfTypes:方法傳入需要刪除的對象,對指定域名下的數(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;

http緩存策略

客戶端和H5在打交道的時候附帽,經(jīng)常會出現(xiàn)頁面緩存的問題,H5的開發(fā)同學(xué)就經(jīng)常說“你清一下緩存試試”井誉,實際上發(fā)生這個問題的原因蕉扮,在于H5的緩存管理策略有問題。這里就講一下H5的緩存管理策略颗圣。

H5的緩存管理其實就是利用http協(xié)議的字段進行管理的喳钟,比較常用的是Cache-ControlLast-Modified搭配使用的方式。

  • Cache-Control:文件緩存有效時長在岂,例如請求文件后服務(wù)器響應(yīng)頭返回Cache-Control:max-age=600奔则,則表示文件有效時長600秒。所以此文件在有效時長內(nèi)蔽午,都不會發(fā)出網(wǎng)絡(luò)請求易茬,直到過期為止。
  • Last-Modified:請求文件后服務(wù)器響應(yīng)頭中返回的及老,表示文件的最新更新時間抽莱。如果Cache-Control過期后,則會請求服務(wù)器并將這個時間放在請求頭的If-Modified-Since字段中骄恶,服務(wù)器收到請求后會進行時間對比食铐,如果時間沒有發(fā)生改變則返回304,否則返回新的文件和響應(yīng)頭字段僧鲁,并返回200虐呻。

Cache-Controlhttp1.1出來的,表示文件的相對有效時長寞秃,在此之前還有Expires字段斟叼,表示文件的絕對有效時長,例如Expires: Thu, 10 Nov 2015 08:45:11 GMT蜕该,二者都可以用犁柜。

Last-Modified也有類似的字段Etag,區(qū)別在于Last-Modified是以時間做對比堂淡,Etag是以文件的哈希值做對比馋缅。當(dāng)文件有效時長過期后,請求服務(wù)器會在請求頭的If-None-Match字段帶上Etag的值绢淀,并交由服務(wù)器對比萤悴。

Cookie處理

眾所周知,http協(xié)議中是支持cookie設(shè)置的皆的,服務(wù)器可以通過Set-Cookie:字段對瀏覽器設(shè)置cookie覆履,并且還可以指定過期時間、域名等。這些在Chrome這些瀏覽器中比較適用硝全,但是如果在客戶端內(nèi)進行顯示栖雾,就需要客戶端傳一些參數(shù)過去,可以讓H5獲取到登錄等狀態(tài)伟众。

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

WKWebView Cookie設(shè)計

之前使用UIWebView的時候账胧,和傳統(tǒng)的cookie管理類NSHTTPCookieStorage讀取的是一塊區(qū)域,或者說UIWebViewcookie也是由此類管理的先紫。但是WKWebViewcookie設(shè)計不太一樣治泥,和Appcookie并沒有存儲在同一塊內(nèi)存區(qū)域,所以二者需要分開做處理遮精。

WKWebViewcookieNSHTTPCookieStorage之間也有同步操作居夹,但是這個同步有明顯的延時,而且規(guī)則不容易琢磨本冲。所以為了代碼的穩(wěn)定性吮播,還是自己處理cookie比較合適。

WKapp是兩個進程眼俊,cookie也是兩份意狠,但是WKcookieapp的沙盒里。有一個定時同步疮胖,但是并沒有一個特定規(guī)則环戈,所以最好不要依賴同步。WKcookie變化只有兩個時機澎灸,一個是js執(zhí)行代碼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的基礎(chǔ)上,為其增加了WKHTTPCookieStore類專門進行cookie的處理元旬,并且支持增加榴徐、刪除守问、查詢?nèi)N操作,還可以注冊一個observercookie的變化進行監(jiān)聽坑资,當(dāng)cookie發(fā)生變化后通過回調(diào)的方法通知監(jiān)聽者耗帕。

WKWebsiteDataStore可以獲取H5頁面通過document.cookie的方式寫入的cookie,以及服務(wù)器通過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及以上設(shè)備狡赐,所以這種方案目前還不能作為我們的選擇窑业。其次是WKWebsiteDataStore,但其只能作為一個刪除cookie的使用枕屉,并不不能用來管理cookie常柄。

我公司的方案是,通過iOS8推出的WKUserContentController來管理webViewcookie搀擂,通過NSHTTPCookieStorage來管理網(wǎng)絡(luò)請求的cookie西潘,例如H5發(fā)出的請求。通過NSURLSession哨颂、NSURLConnection發(fā)出的請求喷市,都會默認帶上NSHTTPCookieStorage中的cookieH5內(nèi)部的請求也會被系統(tǒng)交給NSURLSession處理威恼。

在代碼實現(xiàn)層面品姓,監(jiān)聽didFinishLaunching通知,在程序啟動時從服務(wù)端請求用戶相關(guān)信息箫措,當(dāng)然從本地取也可以腹备,都是一樣的。數(shù)據(jù)是key斤蔓、value的形式下發(fā)植酥,按照key=value的形式拼接,并通過document.cookie組裝成設(shè)置cookiejs代碼弦牡,所有代碼拼接為一個以分號分割的字符串友驮,后面給webViewcookie時就通過這個字符串執(zhí)行。

對于網(wǎng)絡(luò)請求的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的方式實現(xiàn)的艾猜,也就是上面拼接的js字符串买喧。但是這個類有一個問題就是不能持久化cookie,也就是cookieuserContentController的聲明周期匆赃,如果退出Appcookie就會消失淤毛,下次進入App還需要種一次,這是個大問題算柳。

所以我司的處理方式是在decidePolicyForNavigationAction:回調(diào)方法中加入下面這段代碼低淡,代碼中會判斷此域名是否種過cookie,如果沒有則種cookie瞬项。對于cookie的處理蔗蹋,我新建了一個cookieWebview專門處理cookie的問題,當(dāng)執(zhí)行addUserScript后囱淋,通過loadHTMLString:baseURL:加載一個空的本地html猪杭,并將域名設(shè)置為當(dāng)前將要顯示頁面的域名,從而使剛才種的cookie對當(dāng)前processPool內(nèi)所有的webView生效妥衣。

這種方案種cookie是同步執(zhí)行的皂吮,而且對webView的影響很小,經(jīng)過我的測試税手,平均添加一次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,調(diào)用方法將其刪除即可兵扬。webView的刪除方法更是簡單粗暴闺属,直接調(diào)用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加載內(nèi)存占用過多的頁面周霉,會導(dǎo)致WebContent Process進程崩潰掂器,進而頁面出現(xiàn)白屏,也有可能是系統(tǒng)其他進程占用內(nèi)存過多導(dǎo)致的白屏俱箱。對于低內(nèi)存導(dǎo)致的白屏問題国瓮,有以下兩種方案可以解決。

iOS9中蘋果推出了下面的API狞谱,當(dāng)WebContent進程發(fā)生異常退出時乃摹,會回調(diào)此API「疲可以在這個API中進行對應(yīng)的處理孵睬,例如展示一個異常頁面。

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

如果從其他App回來導(dǎo)致白屏問題伶跷,可以在視圖將要顯示的時候掰读,判斷webView.title是否為空秘狞。如果為空則展示異常頁面。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蹈集,一起剝皮案震驚了整個濱河市烁试,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拢肆,老刑警劉巖减响,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異郭怪,居然都是意外死亡支示,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門鄙才,熙熙樓的掌柜王于貴愁眉苦臉地迎上來颂鸿,“玉大人,你說我怎么就攤上這事咒循。” “怎么了绞愚?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵叙甸,是天一觀的道長。 經(jīng)常有香客問我位衩,道長裆蒸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任糖驴,我火速辦了婚禮僚祷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘贮缕。我一直安慰自己辙谜,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布感昼。 她就那樣靜靜地躺著装哆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪定嗓。 梳的紋絲不亂的頭發(fā)上蜕琴,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音宵溅,去河邊找鬼凌简。 笑死,一個胖子當(dāng)著我的面吹牛恃逻,可吹牛的內(nèi)容都是我干的雏搂。 我是一名探鬼主播藕施,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼畔派!你這毒婦竟也來了铅碍?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤线椰,失蹤者是張志新(化名)和其女友劉穎胞谈,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體憨愉,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡烦绳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了配紫。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片径密。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖躺孝,靈堂內(nèi)的尸體忽然破棺而出享扔,到底是詐尸還是另有隱情,我是刑警寧澤植袍,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布惧眠,位于F島的核電站,受9級特大地震影響于个,放射性物質(zhì)發(fā)生泄漏氛魁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一厅篓、第九天 我趴在偏房一處隱蔽的房頂上張望秀存。 院中可真熱鬧,春花似錦羽氮、人聲如沸或链。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽株扛。三九已至,卻和暖如春汇荐,著一層夾襖步出監(jiān)牢的瞬間洞就,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工掀淘, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留旬蟋,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓革娄,卻偏偏與公主長得像倾贰,于是被迫代替她去往敵國和親冕碟。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容