該文章屬于劉小壯原創(chuàng),轉(zhuǎn)載請注明:劉小壯
概述
之前主要使用UIWebView
進行頁面的加載,但是UIWebView
存在很多問題锻煌,在2020年已經(jīng)被蘋果正式拋棄。所以本篇文章主要講解WKWebView
敛腌,WKWebView
從iOS8
開始支持祭饭,現(xiàn)在大多數(shù)App
應(yīng)該都不支持iOS7
了章蚣。
UIWebView
存在兩個問題站欺,一個是內(nèi)存消耗比較大,另一個是性能很差纤垂。WKWebView
相對于UIWebView
來說矾策,性能要比UIWebView
性能要好太多,刷新率能達到60FPS
峭沦。內(nèi)存占用也比UIWebView
要小贾虽。
WKWebView
是一個多進程組件,Network
吼鱼、UI Render
都在獨立的進程中完成蓬豁。
由于WKWebView
和App
不在同一個進程,如果WKWebView
進程崩潰并不會導(dǎo)致應(yīng)用崩潰菇肃,僅僅是頁面白屏等異常地粪。頁面的載入、渲染等消耗內(nèi)存和性能的操作琐谤,都在WKWebView
的進程中處理蟆技,處理后再將結(jié)果交給App
進程用于顯示,所以App
進程的性能消耗會小很多。
網(wǎng)頁加載流程
- 通過域名的方式請求服務(wù)器质礼,請求前瀏覽器會做一個
DNS
解析旺聚,并將IP
地址返回給瀏覽器。 - 瀏覽器使用
IP
地址請求服務(wù)器眶蕉,并且開始握手過程砰粹。TCP
是三次握手,如果使用https
則還需要進行TLS
的握手造挽,握手后根據(jù)協(xié)議字段選擇是否保持連接碱璃。 - 握手完成后,瀏覽器向服務(wù)端發(fā)送請求饭入,獲取
html
文件厘贼。 - 服務(wù)器解析請求,并由
CDN
服務(wù)器返回對應(yīng)的資源文件圣拄。 - 瀏覽器收到服務(wù)器返回的
html
文件,交由html
解析器進行解析毁欣。 - 解析
html
由上到下進行解析xml
標(biāo)簽庇谆,過程中如果遇到css
或資源文件,都會進行異步加載凭疮,遇到js
則會掛起當(dāng)前html
解析任務(wù)饭耳,請求js
并返回后繼續(xù)解析。因為js
文件可能會對DOM
樹進行修改执解。 - 解析完
html
寞肖,并執(zhí)行完js
代碼,形成最終的DOM
樹衰腌。通過DOM
配合css
文件找出每個節(jié)點的最終展示樣式新蟆,并交由瀏覽器進行渲染展示 - 結(jié)束鏈接。
代理方法
WKWebView
和UIWebView
的代理方法發(fā)生了一些改變右蕊,WKWebView
的流程更加細化了琼稻。例如之前UI
結(jié)束請求后,會立刻渲染到webView
上饶囚。而WKWebView
則會在渲染到屏幕之前帕翻,會回調(diào)一個代理方法,代理方法決定是否渲染到屏幕上萝风。這樣就可以對請求下來的數(shù)據(jù)做一次校驗嘀掸,防止數(shù)據(jù)被更改,或驗證視圖是否允許被顯示到屏幕上规惰。
除此之外睬塌,WKWebView
相對于UIWebView
還多了一些定制化操作。
- 重定向的回調(diào),可以在請求重定向時獲取到這次操作衫仑。
- 當(dāng)
WKWebView
進程異常退出時梨与,可以通過回調(diào)獲取叽讳。 - 自定義處理證書统台。
- 更深層的
UI
定制操作,將alert
等UI
操作交給原生層面處理裕膀,而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)用此方法時H5
的dom
已經(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
對象的方式,以及通過webView
的evaluateJavaScript: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
拼接蛤铜、header
、js
注入等方式添加丛肢,這個枚舉是多選的昂羡,也就是可以在多個位置進行注入。除了基礎(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
自帶的localStorage
和Cookie
猪叙,但是Cookie
并不是用來做持久化操作的娇斩,所以也不應(yīng)該給H5
用來做持久化。如果想更穩(wěn)定的進行持久化穴翩,可以考慮提供一個js bridge
的CRUD
接口犬第,讓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ù)蘋果默認的緩存策略,會進行三步檢查课幕。
- 緩存是否存在厦坛。
- 驗證緩存是否過期五垮。
- 緩存是否發(fā)生改變。
緩存文件
iOS9
蘋果提供了緩存管理類WKWebsiteDataStore
杜秸,通過此類可以對磁盤上放仗,指定類型的緩存文件進行查詢和刪除。因為現(xiàn)在很多App
都從iOS9
開始支持撬碟,所以非常推薦此API
來管理本地緩存诞挨,以及cookie
。本地的文件緩存類型定義為以下幾種小作,常用的主要是cookie
亭姥、diskCache
、memoryCache
這些顾稀。
-
WKWebsiteDataTypeFetchCache
达罗,磁盤中的緩存,根據(jù)源碼可以看出静秆,類型是DOMCache
-
WKWebsiteDataTypeDiskCache
粮揉,本地磁盤緩存,和fetchCache
的實現(xiàn)不同抚笔,是所有的緩存數(shù)據(jù) -
WKWebsiteDataTypeMemoryCache
扶认,本地內(nèi)存緩存 -
WKWebsiteDataTypeOfflineWebApplicationCache
,離線web
應(yīng)用程序緩存 -
WKWebsiteDataTypeCookies
殊橙,cookie
緩存 -
WKWebsiteDataTypeSessionStorage
辐宾,html
會話存儲 -
WKWebsiteDataTypeLocalStorage
,html
本地數(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-Control
和Last-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-Control
是http1.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ū)域,或者說UIWebView
的cookie
也是由此類管理的先紫。但是WKWebView
的cookie
設(shè)計不太一樣治泥,和App
的cookie
并沒有存儲在同一塊內(nèi)存區(qū)域,所以二者需要分開做處理遮精。
WKWebView
的cookie
和NSHTTPCookieStorage
之間也有同步操作居夹,但是這個同步有明顯的延時,而且規(guī)則不容易琢磨本冲。所以為了代碼的穩(wěn)定性吮播,還是自己處理cookie
比較合適。
WK
和app
是兩個進程眼俊,cookie
也是兩份意狠,但是WK
的cookie
在app
的沙盒里。有一個定時同步疮胖,但是并沒有一個特定規(guī)則环戈,所以最好不要依賴同步。WK
的cookie
變化只有兩個時機澎灸,一個是js
執(zhí)行代碼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
的基礎(chǔ)上,為其增加了WKHTTPCookieStore
類專門進行cookie
的處理元旬,并且支持增加榴徐、刪除守问、查詢?nèi)N操作,還可以注冊一個observer
對cookie
的變化進行監(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
來管理webView
的cookie
搀擂,通過NSHTTPCookieStorage
來管理網(wǎng)絡(luò)請求的cookie
西潘,例如H5
發(fā)出的請求。通過NSURLSession
哨颂、NSURLConnection
發(fā)出的請求喷市,都會默認帶上NSHTTPCookieStorage
中的cookie
,H5
內(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è)置cookie
的js
代碼弦牡,所有代碼拼接為一個以分號分割的字符串友驮,后面給webView
種cookie
時就通過這個字符串執(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];
}];
}
對webView
種cookie
是通過WKUserContentController
寫入js
的方式實現(xiàn)的艾猜,也就是上面拼接的js
字符串买喧。但是這個類有一個問題就是不能持久化cookie
,也就是cookie
隨userContentController
的聲明周期匆赃,如果退出App
則cookie
就會消失淤毛,下次進入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
是否為空秘狞。如果為空則展示異常頁面。