干貨:探秘WKWebView
作者丨搜狐視頻 劉壯
來源丨搜狐技術(shù)產(chǎn)品(ID:sohu-tech)
本文字?jǐn)?shù):6540字
預(yù)計(jì)閱讀時(shí)間:18分鐘
概述
之前主要使用UIWebView進(jìn)行頁面的加載,但是UIWebView存在很多問題,在2020年已經(jīng)被蘋果正式拋棄。所以本篇文章主要講解WKWebView,WKWebView從iOS8開始支持,現(xiàn)在大多數(shù)App應(yīng)該都不支持iOS7了。
UIWebView存在兩個(gè)問題,一個(gè)是內(nèi)存消耗比較大,另一個(gè)是性能很差。WKWebView相對(duì)于UIWebView來說,性能要比UIWebView性能要好太多,刷新率能達(dá)到60FPS。內(nèi)存占用也比UIWebView要小。
WKWebView是一個(gè)多進(jìn)程組件,Network、UI Render都在獨(dú)立的進(jìn)程中完成。
由于WKWebView和App不在同一個(gè)進(jìn)程,如果WKWebView進(jìn)程崩潰并不會(huì)導(dǎo)致應(yīng)用崩潰,僅僅是頁面白屏等異常。頁面的載入、渲染等消耗內(nèi)存和性能的操作,都在WKWebView的進(jìn)程中處理,處理后再將結(jié)果交給App進(jìn)程用于顯示,所以App進(jìn)程的性能消耗會(huì)小很多。
網(wǎng)頁加載流程
通過域名的方式請(qǐng)求服務(wù)器,請(qǐng)求前瀏覽器會(huì)做一個(gè) DNS解析,并將IP地址返回給瀏覽器。瀏覽器使用 IP地址請(qǐng)求服務(wù)器,并且開始握手過程。TCP是三次握手,如果使用https則還需要進(jìn)行TLS的握手,握手后根據(jù)協(xié)議字段選擇是否保持連接。握手完成后,瀏覽器向服務(wù)端發(fā)送請(qǐng)求,獲取 html文件。服務(wù)器解析請(qǐng)求,并由 CDN服務(wù)器返回對(duì)應(yīng)的資源文件。瀏覽器收到服務(wù)器返回的 html文件,交由html解析器進(jìn)行解析。解析 html由上到下進(jìn)行解析xml標(biāo)簽,過程中如果遇到css或資源文件,都會(huì)進(jìn)行異步加載,遇到js則會(huì)掛起當(dāng)前html解析任務(wù),請(qǐng)求js并返回后繼續(xù)解析。因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(244, 138, 0);">js文件可能會(huì)對(duì)DOM樹進(jìn)行修改。解析完 html,并執(zhí)行完js代碼,形成最終的DOM樹。通過DOM配合css文件找出每個(gè)節(jié)點(diǎn)的最終展示樣式,并交由瀏覽器進(jìn)行渲染展示結(jié)束鏈接。
代理方法
WKWebView和UIWebView的代理方法發(fā)生了一些改變,WKWebView的流程更加細(xì)化了。例如之前UI結(jié)束請(qǐng)求后,會(huì)立刻渲染到webView上。而WKWebView則會(huì)在渲染到屏幕之前,會(huì)回調(diào)一個(gè)代理方法,代理方法決定是否渲染到屏幕上。這樣就可以對(duì)請(qǐng)求下來的數(shù)據(jù)做一次校驗(yàn),防止數(shù)據(jù)被更改,或驗(yàn)證視圖是否允許被顯示到屏幕上。
除此之外,WKWebView相對(duì)于UIWebView還多了一些定制化操作。
重定向的回調(diào),可以在請(qǐng)求重定向時(shí)獲取到這次操作。 當(dāng) WKWebView進(jìn)程異常退出時(shí),可以通過回調(diào)獲取。自定義處理證書。 更深層的 UI定制操作,將alert等UI操作交給原生層面處理,而UI方案UIAlertView是直接webView顯示的。
WKUIDelegate
WKWebView將很多UI的顯示都交給原生層面去處理,例如彈窗或者輸入框的顯示。這樣如果項(xiàng)目里有統(tǒng)一定義的彈窗,就可以直接調(diào)用自定義彈窗,而不是只能展示系統(tǒng)彈窗。
在WKWebView中,系統(tǒng)將彈窗的顯示交由客戶端來控制??蛻舳丝梢酝ㄟ^下面的回調(diào)方法獲取到彈窗的顯示信息,并由客戶端來調(diào)起UIAlertController來展示。參數(shù)中有一個(gè)completionHandler的回調(diào)block,需要客戶端一定要調(diào)用,如果不調(diào)用則會(huì)發(fā)生崩潰。
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;
有時(shí)候H5會(huì)要求用戶進(jìn)行一些輸入,例如用戶名密碼之類的??蛻舳丝梢酝ㄟ^下面的方法獲取到輸入框事件,并由客戶端展示輸入框,用戶輸入完成后將結(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中,這里挑幾個(gè)比較常用的方法講一下。
下面的方法,通過decisionHandler回調(diào)中返回一個(gè)枚舉類型的參數(shù),表示是否允許頁面加載。這里可以對(duì)域名進(jìn)行判斷,如果是站外域名,則可以提示用戶是否進(jìn)行跳轉(zhuǎn)。如果是跳轉(zhuǎn)其他App或商店的URL,則可以通過openURL進(jìn)行跳轉(zhuǎn),并將這次請(qǐng)求攔截。包括cookie的處理也在此方法中完成,后面會(huì)詳細(xì)講到cookie的處理。
除此之外,很多頁面顯示前的邏輯處理,也在此方法中完成。但需要注意的是,方法中不要做過多的耗時(shí)處理,會(huì)影響頁面加載速度。
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
開始加載頁面,并請(qǐng)求服務(wù)器。
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
當(dāng)頁面加載失敗的時(shí)候,會(huì)回調(diào)此方法,包括timeout等錯(cuò)誤。在這個(gè)頁面可以展示錯(cuò)誤頁面,清空進(jìn)度條,重置網(wǎng)絡(luò)指示器等操作。需要注意的是,調(diào)用goBack時(shí)也會(huì)執(zhí)行此方法,可以通過error的狀態(tài)判斷是否NSURLErrorCancelled來過濾掉。
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error;
頁面加載及渲染完成,會(huì)調(diào)用此方法,調(diào)用此方法時(shí)H5的dom已經(jīng)解析并渲染完成,展示在屏幕上。所以在此方法中可以進(jìn)行一些加載完成的操作,例如移除進(jìn)度條,重置網(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:方法,并傳入一個(gè)實(shí)現(xiàn)了WKScriptMessageHandler協(xié)議的對(duì)象,即可接收js的回調(diào),由于userContent會(huì)強(qiáng)引用傳入的對(duì)象,所以應(yīng)該是新創(chuàng)建一個(gè)對(duì)象,而不是self。注冊(cè)對(duì)象時(shí),后面的name就是js調(diào)用的函數(shù)名。
WKUserContentController *userContent = [[WKUserContentController alloc] init];
[userContent addScriptMessageHandler:[[WKWeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"clientCallback"];
在dealloc中應(yīng)該通過下面的方法,移除對(duì)指定name的處理。
[userContent removeScriptMessageHandlerForName:@"clientCallback"];
H5通過下面的代碼即可對(duì)客戶端發(fā)起調(diào)用,調(diào)用是通過postMessage函數(shù)傳一個(gè)json串過來,需要加上轉(zhuǎn)移字符。客戶端接收到調(diào)用后,根據(jù)回調(diào)方法傳入的WKScriptMessage對(duì)象,獲取到body字典,解析傳入的參數(shù)即可。
window.webkit.messageHandlers.clientCallback.postMessage("{\"funName\":\"getMobileCode\",\"value\":\"srggshqisslfkj\"}");調(diào)用
原生調(diào)用H5的方法也是一樣,創(chuàng)建一個(gè)WKUserScript對(duì)象,并將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對(duì)于執(zhí)行js代碼提供了兩種方式,通過userContent添加一個(gè)WKUserScript對(duì)象的方式,以及通過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)部的實(shí)現(xiàn)方式我沒有深入研究,WebKit內(nèi)核是開源的,有興趣的同學(xué)可以看看。但是這兩種方式還是有一些功能上的區(qū)別的,可以根據(jù)具體業(yè)務(wù)場景去選擇對(duì)應(yīng)的API。
先說說evaluateJavaScript:completionHandler:的方式,這種方式一般是在頁面展示完成后執(zhí)行的操作,用來調(diào)用js的函數(shù)并獲取返回值非常方便。當(dāng)然也可以用來注入一段js代碼,但需要自己控制注入時(shí)機(jī)。
WKUserScript則可以控制注入時(shí)機(jī),可以針對(duì)document是否加載完選擇注入js。以及被注入的js是在當(dāng)前頁面有效,還是包括其子頁面也有效。相對(duì)于evaluateJavaScript:方法,此方法不能獲得js執(zhí)行后的返回值,所以兩個(gè)方法在功能上還是有區(qū)別的。
容器設(shè)計(jì)
設(shè)計(jì)思路
項(xiàng)目中一般不會(huì)直接使用WKWebView,而是通過對(duì)其進(jìn)行一層包裝,成為一個(gè)WKWebViewController交給業(yè)務(wù)層使用。設(shè)計(jì)webViewVC時(shí)應(yīng)該遵循簡單靈活的思想去設(shè)計(jì),自身只提供展示功能,不涉及任何業(yè)務(wù)邏輯。對(duì)外提供展示導(dǎo)航欄、設(shè)置標(biāo)題、進(jìn)度條等功能,都可以通過WKWebViewConfiguration賦值并在WKWebViewController實(shí)例化的時(shí)候傳入。
對(duì)調(diào)用方提供js交互、webView生命周期、加載錯(cuò)誤等回調(diào),外接通過對(duì)應(yīng)的回調(diào)進(jìn)行處理。這些回調(diào)都是可選的,不實(shí)現(xiàn)對(duì)webView加載也沒有影響。下面是實(shí)例代碼,也可以把不同類型的回調(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)該負(fù)責(zé)處理公共參數(shù),并且可以基于公共參數(shù)進(jìn)行擴(kuò)展。這里我們定義了一個(gè)方法,可以指定基礎(chǔ)參數(shù)的位置,是通過URL拼接、header、js注入等方式添加,這個(gè)枚舉是多選的,也就是可以在多個(gè)位置進(jìn)行注入。除了基礎(chǔ)參數(shù),還可以額外添加自定義參數(shù),也會(huì)添加到指定的位置。
- (void)injectionParamsType:(SVParamsType)type additionalParams:(NSDictionary *)additionalParams;
復(fù)用池
WKWebView第一次初始化的時(shí)候,會(huì)先啟動(dòng)webKit內(nèi)核,并且有一些初始化操作,這個(gè)操作是非常消耗性能的。所以,復(fù)用池設(shè)計(jì)的第一步,是在App啟動(dòng)的時(shí)候,初始化一個(gè)全局的WKWebView。
并且,創(chuàng)建兩個(gè)池子,創(chuàng)建visiblePool存放正在使用的,創(chuàng)建reusablePool存放空閑狀態(tài)的。并且,在頁面退出時(shí),從visiblePool放入reusablePool的同時(shí),應(yīng)該將頁面進(jìn)行回收,清除頁面上的數(shù)據(jù)。
當(dāng)需要初始化一個(gè)webView容器時(shí),從reusablePool中取出一個(gè)容器,并且放入到visiblePool中。通過復(fù)用池的實(shí)現(xiàn),可以減少從初始化一個(gè)webView容器,到頁面展示出來的時(shí)間。
WKProcessPool
在WKWebView中定義了processPool屬性,可以指定對(duì)應(yīng)的進(jìn)程池對(duì)象。每個(gè)webView都有自己的內(nèi)容進(jìn)程,如果不指定則默認(rèn)是一個(gè)新的內(nèi)容進(jìn)程。內(nèi)容進(jìn)程中包括一些本地cookie、資源之類的,如果不在一個(gè)內(nèi)容進(jìn)程中,則不能共享這些數(shù)據(jù)。
可以創(chuàng)建一個(gè)公共的WKProcessPool,是一個(gè)單例對(duì)象。所有webView創(chuàng)建的時(shí)候,都使用同一個(gè)內(nèi)容進(jìn)程,即可實(shí)現(xiàn)資源共享。
UserAgent
User-Agent是在http協(xié)議中的一個(gè)請(qǐng)求頭字段,用來告知服務(wù)器一些信息的,User-Agent中包含了很多字段,例如系統(tǒng)版本、瀏覽器內(nèi)核版本、網(wǎng)絡(luò)環(huán)境等。這個(gè)字段可以直接用系統(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寫入的方式對(duì)H5注入User-Agent。
通信協(xié)議
一個(gè)設(shè)計(jì)的比較好的WebView容器,應(yīng)該具備很好的相互通信功能,并且靈活具有擴(kuò)展性。H5和客戶端的通信主要有以下幾種場景。
js調(diào)用客戶端,以及js調(diào)用客戶端后獲取客戶端的callback回調(diào)及參數(shù)。客戶端調(diào)用 js,以及調(diào)用js后的callback回調(diào)及參數(shù)。客戶端主動(dòng)通知 H5,客戶端的一些生命周期變化。例如進(jìn)入鎖屏和進(jìn)入前臺(tái)等系統(tǒng)生命周期。
以js調(diào)用客戶端為例,有兩個(gè)緯度的調(diào)用??梢酝ㄟ^URLRouter的方式直接調(diào)用某個(gè)模塊,這種調(diào)用方式遵循客戶端的URL定義即可調(diào)起,并且支持傳參。還可以通過userContentController的方式,進(jìn)行頁面級(jí)的調(diào)用,例如關(guān)閉webView、調(diào)起登錄功能等,也就是通過js調(diào)用客戶端的某個(gè)功能,這種方式需要客戶端提供對(duì)應(yīng)的處理代碼。
二者之間相互調(diào)用,盡量避免高頻調(diào)用,而且一般也不會(huì)有高頻調(diào)用的需求。但如果發(fā)生相同功能高頻調(diào)用,則需要設(shè)置一個(gè)actionID來區(qū)分不同的調(diào)用,以保證發(fā)生回調(diào)時(shí)可以正常被區(qū)分。
callback的回調(diào)方法也可以通過參數(shù)傳遞過來,這種方式靈活性比較強(qiáng),如果固定寫死會(huì)有版本限制,較早版本的客戶端可能并不支持這個(gè)回調(diào)。
處理回調(diào)
webView的回調(diào)除了基礎(chǔ)的調(diào)用,例如refresh刷新當(dāng)前頁面、close關(guān)閉當(dāng)前頁面等,直接由對(duì)應(yīng)的功能類來處理調(diào)用,其他的時(shí)間應(yīng)該交給外界處理。
這里的設(shè)計(jì)方案并不是一個(gè)事件對(duì)應(yīng)一個(gè)回調(diào)方法,然后外界遵循代理并實(shí)現(xiàn)多個(gè)代理方法的方式來實(shí)現(xiàn)。而是將每次回調(diào)事件都封裝成一個(gè)對(duì)象,直接將這個(gè)對(duì)象回調(diào)給外界處理,這樣靈活性更強(qiáng)一些,而且外界獲取的信息也更多。事件模型的定義可以參考下面的。
@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)定的進(jìn)行持久化,可以考慮提供一個(gè)js bridge的CRUD接口,讓H5可以用來存儲(chǔ)和查詢數(shù)據(jù)。
持久化方案就采取和客戶端一致的方案,給H5單獨(dú)建一張數(shù)據(jù)表即可。
緩存機(jī)制
緩存規(guī)則
前端瀏覽器包括WKWebView在內(nèi),為了保證快速打開頁面,減少用戶流量消耗,都會(huì)對(duì)資源進(jìn)行緩存。這個(gè)緩存規(guī)則在WKWebView中也可以指定,如果我們?yōu)榱吮WC每次的資源文件都是最新的,也可以選擇不使用緩存,但我們一般不這么做。
NSURLRequestUseProtocolCachePolicy = 0,默認(rèn)緩存策略,和Safari內(nèi)核的緩存表現(xiàn)一樣。NSURLRequestReloadIgnoringLocalCacheData = 1,忽略本地緩存,直接從服務(wù)器獲取數(shù)據(jù)。NSURLRequestReturnCacheDataElseLoad = 2, 本地有緩存則使用緩存,否則加載服務(wù)端數(shù)據(jù)。這種策略不會(huì)驗(yàn)證緩存是否過期。NSURLRequestReturnCacheDataDontLoad = 3, 只從本地獲取,并且不判斷有效性和是否改變,本地沒有不會(huì)請(qǐng)求服務(wù)器數(shù)據(jù),請(qǐng)求會(huì)失敗。NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, 忽略本地以及路由過程中的緩存,從服務(wù)器獲取最新數(shù)據(jù)。NSURLRequestReloadRevalidatingCacheData = 5, 從服務(wù)端驗(yàn)證緩存是否可用,本地不可用則請(qǐng)求服務(wù)端數(shù)據(jù)。NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,

根據(jù)蘋果默認(rèn)的緩存策略,會(huì)進(jìn)行三步檢查。
緩存是否存在。 驗(yàn)證緩存是否過期。 緩存是否發(fā)生改變。
緩存文件
iOS9蘋果提供了緩存管理類WKWebsiteDataStore,通過此類可以對(duì)磁盤上,指定類型的緩存文件進(jìn)行查詢和刪除。因?yàn)楝F(xiàn)在很多App都從iOS9開始支持,所以非常推薦此API來管理本地緩存,以及cookie。本地的文件緩存類型定義為以下幾種,常用的主要是cookie、diskCache、memoryCache這些。
WKWebsiteDataTypeFetchCache,磁盤中的緩存,根據(jù)源碼可以看出,類型是DOMCacheWKWebsiteDataTypeDiskCache,本地磁盤緩存,和fetchCache的實(shí)現(xiàn)不同,是所有的緩存數(shù)據(jù)WKWebsiteDataTypeMemoryCache,本地內(nèi)存緩存WKWebsiteDataTypeOfflineWebApplicationCache,離線web應(yīng)用程序緩存WKWebsiteDataTypeCookies,cookie緩存WKWebsiteDataTypeSessionStorage,html會(huì)話存儲(chǔ)WKWebsiteDataTypeLocalStorage,html本地?cái)?shù)據(jù)緩存WKWebsiteDataTypeWebSQLDatabases,WebSQL數(shù)據(jù)庫數(shù)據(jù)WKWebsiteDataTypeIndexedDBDatabases,數(shù)據(jù)庫索引WKWebsiteDataTypeServiceWorkerRegistrations,服務(wù)器注冊(cè)數(shù)據(jù)
通過下面的方法可以獲取本地所有的緩存文件類型,返回的集合字符串,就是上面定義的類型。
+ (NSSet<NSString *> *)allWebsiteDataTypes;
可以指定刪除某個(gè)時(shí)間段內(nèi),指定類型的數(shù)據(jù),刪除后會(huì)回調(diào)block。
- (void)removeDataOfTypes:(NSSet<NSString *> *)dataTypes modifiedSince:(NSDate *)date completionHandler:(void (^)(void))completionHandler;
系統(tǒng)還提供了定制化更強(qiáng)的方法,通過fetchDataRecordsOfTypes:方法獲取指定類型的所有WKWebsiteDataRecord對(duì)象,此對(duì)象包含域名和類型兩個(gè)參數(shù)。可以根據(jù)域名和類型進(jìn)行判斷,隨后調(diào)用removeDataOfTypes:方法傳入需要?jiǎng)h除的對(duì)象,對(duì)指定域名下的數(shù)據(jù)進(jìn)行刪除。
// 獲取
- (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在打交道的時(shí)候,經(jīng)常會(huì)出現(xiàn)頁面緩存的問題,H5的開發(fā)同學(xué)就經(jīng)常說“你清一下緩存試試”,實(shí)際上發(fā)生這個(gè)問題的原因,在于H5的緩存管理策略有問題。這里就講一下H5的緩存管理策略。
H5的緩存管理其實(shí)就是利用http協(xié)議的字段進(jìn)行管理的,比較常用的是Cache-Control和Last-Modified搭配使用的方式。
Cache-Control:文件緩存有效時(shí)長,例如請(qǐng)求文件后服務(wù)器響應(yīng)頭返回Cache-Control:max-age=600,則表示文件有效時(shí)長600秒。所以此文件在有效時(shí)長內(nèi),都不會(huì)發(fā)出網(wǎng)絡(luò)請(qǐng)求,直到過期為止。Last-Modified:請(qǐng)求文件后服務(wù)器響應(yīng)頭中返回的,表示文件的最新更新時(shí)間。如果Cache-Control過期后,則會(huì)請(qǐng)求服務(wù)器并將這個(gè)時(shí)間放在請(qǐng)求頭的If-Modified-Since字段中,服務(wù)器收到請(qǐng)求后會(huì)進(jìn)行時(shí)間對(duì)比,如果時(shí)間沒有發(fā)生改變則返回304,否則返回新的文件和響應(yīng)頭字段,并返回200。
Cache-Control是http1.1出來的,表示文件的相對(duì)有效時(shí)長,在此之前還有Expires字段,表示文件的絕對(duì)有效時(shí)長,例如Expires: Thu, 10 Nov 2015 08:45:11 GMT,二者都可以用。
Last-Modified也有類似的字段Etag,區(qū)別在于Last-Modified是以時(shí)間做對(duì)比,Etag是以文件的哈希值做對(duì)比。當(dāng)文件有效時(shí)長過期后,請(qǐng)求服務(wù)器會(huì)在請(qǐng)求頭的If-None-Match字段帶上Etag的值,并交由服務(wù)器對(duì)比。
Cookie處理
眾所周知,http協(xié)議中是支持cookie設(shè)置的,服務(wù)器可以通過Set-Cookie:字段對(duì)瀏覽器設(shè)置cookie,并且還可以指定過期時(shí)間、域名等。這些在Chrome這些瀏覽器中比較適用,但是如果在客戶端內(nèi)進(jìn)行顯示,就需要客戶端傳一些參數(shù)過去,可以讓H5獲取到登錄等狀態(tài)。
蘋果雖然提供了一些Cookie管理的API,但在WKWebView的使用上還是有很多坑的,最后我會(huì)給出一個(gè)比較通用的方案。
WKWebView Cookie設(shè)計(jì)
之前使用UIWebView的時(shí)候,和傳統(tǒng)的cookie管理類NSHTTPCookieStorage讀取的是一塊區(qū)域,或者說UIWebView的cookie也是由此類管理的。但是WKWebView的cookie設(shè)計(jì)不太一樣,和App的cookie并沒有存儲(chǔ)在同一塊內(nèi)存區(qū)域,所以二者需要分開做處理。
WKWebView的cookie和NSHTTPCookieStorage之間也有同步操作,但是這個(gè)同步有明顯的延時(shí),而且規(guī)則不容易琢磨。所以為了代碼的穩(wěn)定性,還是自己處理cookie比較合適。
WK和app是兩個(gè)進(jìn)程,cookie也是兩份,但是WK的cookie在app的沙盒里。有一個(gè)定時(shí)同步,但是并沒有一個(gè)特定規(guī)則,所以最好不要依賴同步。WK的cookie變化只有兩個(gè)時(shí)機(jī),一個(gè)是js執(zhí)行代碼setCookie,另一個(gè)是response返回cookie。
WKWebsiteDataStore
Cookie的管理一直都是WKWebView的一個(gè)弊端,對(duì)于Cookie的處理很不方便。在iOS9中可以通過WKWebsiteDataStore對(duì)Cookie進(jìn)行管理,但是用起來并不直觀,需要進(jìn)行dataType進(jìn)行篩選并刪除。而且WKWebsiteDataStore自身功能并不具備添加功能,所以對(duì)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類專門進(jìn)行cookie的處理,并且支持增加、刪除、查詢?nèi)N操作,還可以注冊(cè)一個(gè)observer對(duì)cookie的變化進(jìn)行監(jiān)聽,當(dāng)cookie發(fā)生變化后通過回調(diào)的方法通知監(jiān)聽者。
WKWebsiteDataStore可以獲取H5頁面通過document.cookie的方式寫入的cookie,以及服務(wù)器通過Set-Cookie的方式寫入的cookie,所以還是很推薦使用這個(gè)類來管理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,但其只能作為一個(gè)刪除cookie的使用,并不不能用來管理cookie。
我公司的方案是,通過iOS8推出的WKUserContentController來管理webView的cookie,通過NSHTTPCookieStorage來管理網(wǎng)絡(luò)請(qǐng)求的cookie,例如H5發(fā)出的請(qǐng)求。通過NSURLSession、NSURLConnection發(fā)出的請(qǐng)求,都會(huì)默認(rèn)帶上NSHTTPCookieStorage中的cookie,H5內(nèi)部的請(qǐng)求也會(huì)被系統(tǒng)交給NSURLSession處理。
在代碼實(shí)現(xiàn)層面,監(jiān)聽didFinishLaunching通知,在程序啟動(dòng)時(shí)從服務(wù)端請(qǐng)求用戶相關(guān)信息,當(dāng)然從本地取也可以,都是一樣的。數(shù)據(jù)是key、value的形式下發(fā),按照key=value的形式拼接,并通過document.cookie組裝成設(shè)置cookie的js代碼,所有代碼拼接為一個(gè)以分號(hào)分割的字符串,后面給webView種cookie時(shí)就通過這個(gè)字符串執(zhí)行。
對(duì)于網(wǎng)絡(luò)請(qǐng)求的cookie,通過NSHTTPCookieStorage直接將cookie種到根域名下的,可以對(duì)根域名下所有子域名生效,這里的處理比較簡單。
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];
}];
}
對(duì)webView種cookie是通過WKUserContentController寫入js的方式實(shí)現(xiàn)的,也就是上面拼接的js字符串。但是這個(gè)類有一個(gè)問題就是不能持久化cookie,也就是cookie隨userContentController的聲明周期,如果退出App則cookie就會(huì)消失,下次進(jìn)入App還需要種一次,這是個(gè)大問題。
所以我司的處理方式是在decidePolicyForNavigationAction:回調(diào)方法中加入下面這段代碼,代碼中會(huì)判斷此域名是否種過cookie,如果沒有則種cookie。對(duì)于cookie的處理,我新建了一個(gè)cookieWebview專門處理cookie的問題,當(dāng)執(zhí)行addUserScript后,通過loadHTMLString:baseURL:加載一個(gè)空的本地html,并將域名設(shè)置為當(dāng)前將要顯示頁面的域名,從而使剛才種的cookie對(duì)當(dāng)前processPool內(nèi)所有的webView生效。
這種方案種cookie是同步執(zhí)行的,而且對(duì)webView的影響很小,經(jīng)過我的測試,平均添加一次cookie只需要消耗28ms的時(shí)間。從用戶的角度來看是無感知的,并不會(huì)有頁面的卡頓或重新刷新。
- (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的處理則相對(duì)比較簡單,NSHTTPCookieStorage通過cookies屬性遍歷到自己需要?jiǎng)h除的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)存占用過多的頁面,會(huì)導(dǎo)致WebContent Process進(jìn)程崩潰,進(jìn)而頁面出現(xiàn)白屏,也有可能是系統(tǒng)其他進(jìn)程占用內(nèi)存過多導(dǎo)致的白屏。對(duì)于低內(nèi)存導(dǎo)致的白屏問題,有以下兩種方案可以解決。
在iOS9中蘋果推出了下面的API,當(dāng)WebContent進(jìn)程發(fā)生異常退出時(shí),會(huì)回調(diào)此API。可以在這個(gè)API中進(jìn)行對(duì)應(yīng)的處理,例如展示一個(gè)異常頁面。
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView;
如果從其他App回來導(dǎo)致白屏問題,可以在視圖將要顯示的時(shí)候,判斷webView.title是否為空。如果為空則展示異常頁面。
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

面試題】即可獲取
