WKWebView使用指南|功能豐富的JXBWKWebView

github地址:JXBWKWebView,若是以爲項目不錯能夠點個star支持一下,謝謝~前端

前言


目前iOS系統已經更新到iOS11,大多數項目向下兼容最多兼容到iOS8,所以,在項目中對WebView組件進行重構再封裝時,打算直接捨棄UIWebView轉用WKWebViewjava

若是你目前正在網上瀏覽關於WKWebView的一些文章,相信你已經清楚了WKWebView的優勢,也目擊了你們在使用WKWebView的過程當中遇到的坑,而這篇文章,會對到目前爲止你們遇到的關於WKWebView的問題給出詳細的解決方案,文章的最後,也會講述關於對WKWebView進行性能優化的方案。git

解決的問題


  • goback返回頁面不刷新
  • Cookie
  • POST請求失效
  • crash
  • navigationBackItem
  • 進度條
  • NativeJS的交互
  • 優化H5頁面啓動速度

入坑


goback Api返回不刷新

在以前使用UIWebView時,調用goback後,頁面會刷新。使用WKWebView後,調用goback,即使調用reload方法,H5依然不會刷新。github

緣由是調用goback時,UIWebView會觸發onload事件,WKWebView不會觸發onload事件,若是前端依舊在onload事件中處理iOS的頁面返回事件,是處理不了的,解決方案是讓前端使用onpageshow事件監聽WKWebView的頁面goback事件。web

前端代碼以下:ajax

window.addEventListener("pageshow", function(event){
    if(event.persisted){
        location.reload();
    }
});

爲了查看頁面是直接從服務器上載入仍是從緩存中讀取,可使用 PageTransitionEvent對象的persisted屬性來判斷。chrome

若是頁面從瀏覽器的緩存中讀取該屬性返回ture,不然返回 false。而後在根據truefalse在執行相應的頁面刷新動做或者直接ajax請求接口更新數據。json

關於onloadonpageshow事件在safarichrome上的區別以下:api

. 事件 Chrome Safari
第一次加載頁面 onload 觸發 觸發
第一次加載頁面 onpageshow 觸發 觸發
從其餘頁面返回 onload 觸發 不觸發
從其餘頁面返回 onpageshow 觸發 觸發
關於cookie

WKWebView屬於webkit框架,其將瀏覽器內核渲染進程提取出 App主進程,由另一個進程進行管理,減小了至關一部分的性能損失,這也是性能上比UIWebView優越的緣由之一。跨域

既然WKWebView的工做進程獨立於App Process以外,咱們暫且稱爲WK Process(隨便起的)。

在使用AFN進行網絡請求時,若是server使用set-cookiecookie寫入headerAFN接受到響應後會將cookie保存到NSHTTPCookieStorage,下次若是是同域的request urlAFN會將cookieNSHTTPCookieStorage 中取出而後做爲request headercookie發送給server端,而這一切發生在App Process

那麼在WK Process工做的WKWebView在發送網絡請求及收到響應後對cookie的處理是否也會使用NSHTTPCookieStorage 呢,通過測試後,答案是yes,但在存取的過程當中會有一些問題須要注意。

先說存:

測試進行:iphone 6p iOS:10
測試過程:
1.client使用AFN發送一個網絡請求
2.server接收到請求後,使用set-cookie寫入cookie
3.client接收到success response後,使用以下方式輸出log

NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:fields forURL:url];
for (NSHTTPCookie *cookie in cookies) {
    NSLog(@"cookie,name:= %@,valuie = %@",cookie.name,cookie.value);
}

4.進入WKWebView所在頁面,使用loadRequest隨便發送一個同域的網絡請求,在decidePolicyForNavigationResponse代理方法中,使用以下代碼輸出log

NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
NSArray *cookies =[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
for (NSHTTPCookie *cookie in cookies) {
    NSLog(@"wkwebview中的cookie:%@", cookie);
}

也可使用以下代碼輸出該請求的server response headerset-cookie

NSString *cookieString = [[response allHeaderFields] valueForKey:@"Set-Cookie"];

那麼,WKWebViewcookie存入NSHTTPCookieStorage 的時機是何時?
1.JS執行document.cookie或服務器set-cookie注入的Cookie會很快同步到NSHTTPCookieStorage中。
2.H5頁面進行跳轉時會將Cookie同步到NSHTTPCookieStorage中。
3.控制器頁面跳轉時會將Cookie同步到NSHTTPCookieStorage中。

再說取:

WKWebView使用loadRequest發送網絡時不會主動將cookie存入到NSHTTPCookieStorage 中,即便是同域的請求。

因此,若是你有一個請求須要附帶cookie,就不能直接加載URL,須要你根據URL建立一個URLMutableRequest對象,將須要附加的cookie使用addValue:forHTTPHeaderField:方法手動將cookie添加到request header中,但這僅能解決首次請求不帶cookie的問題,若是頁面發送ajax請求,cookie一樣帶不上,解決方案是經過document.cookie設置cookie,也就是說在你實例化WKWebView時就應該注入相關script

上面咱們說的都是在同域的狀況下,若是發生302請求(能夠理解域名發生變化,也就是說不一樣域),上面的解決方案就用不了了,這時就須要你在WKWebViewdecidePolicyForNavigationAction代理方法中攔截URL,判斷當前URL與初次請求的URL是否同域,若是不一樣域,在該代理方法中獲取到當前請求的request對象並copy出一個新的對象,經過addValue:forHeaderField:方法將cookie手動添加到header中,而後讓WKWebView使用loadRequest從新加載這個copy出來的新的request對象。

問題就沒了嗎?NO,上面的解決方法一樣有侷限,即只能解決後續的同域ajax請求不加cookie的問題。若是發生iframe跨域請求,咱們攔截不到請求,因此也無法給請求的header手動添加cookieWKWebView只適合加載mainFrame 請求。

因此,要和前端同窗提早打好招呼,儘可能避免使用iframe,能使用ajax的地方儘可能使用ajax,另外一方面,iframe如今已經不怎麼提倡使用了,除非是解決一些特殊的問題。

POST請求

使用WKWebView沒法正常發送POST請求。

因此,這個時候咱們須要經過自定義NSURLProtocol攔截WKWebView的網絡請求,而且,使用NSURLProtocol攔截WKWebView網絡請求的好處還有就是:
1.若是產品需求要求client須要日誌採集,包括全部的網絡請求記錄,經過這種方式你是能夠獲取到的。
2.若是公司對用戶體驗的要求較高,能夠在這裏實現WKWebView初始化和相關網絡請求的併發執行,以縮短用戶在client打開H5的速度,甚至能夠秒開,達到和native相同的體驗。

但問題是正常狀況下NSURLProtocol是攔截不到WKWebView的網絡請求的。

經過觀看webkit的源碼(github直接搜webkit)能夠獲得的結果是,經過WKWebView發送一個網絡請求其實也會走NSURLProtocol,只不過Applehttphttps這兩個scheme給過濾掉了,致使咱們攔截不到WKWebView發送的網路請求。

所以,在咱們自定義NSURLProtocol時,要經過使用私有api來註冊一些scheme,註冊scheme的類名叫WKBrowsingContextController WKWebView中有一個屬性叫browsingContextController,就是這個類的對象。註冊的方法叫registerSchemeForCustomProtocol:,知道這個私有api,咱們就能夠經過target-action的方式,註冊WKWebView發起網絡請求時須要攔截的URL scheme,此時註冊的scheme至少要包括3種,分別是httphttpspost

問題還沒玩,解決一個問題的同時每每伴隨另外一個問題的產生。

使用這種方案攔截WKWebView的網絡請求形成的問題就是post請求body數據被清空,仍是Apple所爲,看webkit源碼:

void ArgumentCoder<ResourceRequest>::encodePlatformData(Encoder& encoder, const ResourceRequest& resourceRequest)
{
    RetainPtr<CFURLRequestRef> requestToSerialize = resourceRequest.cfURLRequest(DoNotUpdateHTTPBody);

    bool requestIsPresent = requestToSerialize;
    encoder << requestIsPresent;

    if (!requestIsPresent)
        return;

    // We don't send HTTP body over IPC for better performance.
    // Also, it's not always possible to do, as streams can only be created in process that does networking.
    RetainPtr<CFDataRef> requestHTTPBody = adoptCF(CFURLRequestCopyHTTPRequestBody(requestToSerialize.get()));
    RetainPtr<CFReadStreamRef> requestHTTPBodyStream = adoptCF(CFURLRequestCopyHTTPRequestBodyStream(requestToSerialize.get()));
    if (requestHTTPBody || requestHTTPBodyStream) {
        CFMutableURLRequestRef mutableRequest = CFURLRequestCreateMutableCopy(0, requestToSerialize.get());
        requestToSerialize = adoptCF(mutableRequest);
        CFURLRequestSetHTTPRequestBody(mutableRequest, nil);
        CFURLRequestSetHTTPRequestBodyStream(mutableRequest, nil);
    }

    RetainPtr<CFDictionaryRef> dictionary = adoptCF(WKCFURLRequestCreateSerializableRepresentation(requestToSerialize.get(), IPC::tokenNullTypeRef()));
    IPC::encode(encoder, dictionary.get());

    // The fallback array is part of CFURLRequest, but it is not encoded by WKCFURLRequestCreateSerializableRepresentation.
    encoder << resourceRequest.responseContentDispositionEncodingFallbackArray();
    encoder.encodeEnum(resourceRequest.requester());
}

主要看代碼中間那兩句註釋,大體的意思就是Apple不會在進程間通訊發送httpbody

由於WKWebView屬於webkit框架,所以WKWebView的網絡請求、內容加載/渲染都是在WK Process中進行,但NSURLProtocol攔截請求還在App Process,一旦註冊http(s) scheme後,網絡請求將從獨立進程中發送到App Process,這樣自定義的NSURLProtocol才能攔截到網絡請求,爲了提高進程間通訊效率,出於性能上的考慮,Apple會將requestbody數據丟棄,由於body數據(二進制類型)大小沒有限制,size偏大的話就會對數據傳輸效率有嚴重影響進而影響到攔截請求時的操做及延時後續的網絡請求,所以,Apple在進行進程間通訊時會把post請求的body丟棄。

如何解決?
終極思路就是雖然httpbody會在進程間通訊時被丟棄,但header不會。

所以,解決問題步驟以下:

  • WKWebViewloadRequest前對request對象進行一些處理,這個request對象咱們記爲old request

1.記下old requestschemeNSData類型的http body
2.獲取當前old requestURL,替換URLschemepost(這也是咱們爲何要在前面使用NSURLProtocol註冊post scheme的緣由),並根據這個替換好的URL從新生成一個新的NSMutableURLRequest對象,這個對象記爲new request
3.給new requestheader賦值,把步驟1中獲取的schemehttp body手動添加到這個new requestheader中,若是這個post請求須要附帶cookie的話,你也要把cookieold request中拿出來放到new requestheader中。
4.讓WKWebView加載這個new request

  • WKWebView發送新的request時(這個request urlschemepost),咱們能夠在自定義NSURLProtocol中攔截到這個請求,執行以下步驟:

1.替換scheme,此時的schemepost,你須要把post scheme替換成old requestscheme,這個字段咱們以前已經保存下來了。
2.替換scheme後會生成一個新的URL,根據這個新的URL生成一個NSURLMutableRequest對象,將以前保存的http bodycookie放到這個新的request對象的header中。
3.使用NSURLSession,根據新的request對象發送網絡請求,而後經過NSURLProtocol Client將加載結果返回給WKWebView

注意:在這幾個步驟中一共產生了3個request對象。

crash

1.alert彈窗
引發crash的緣由是js調用alert()引發的,也就是說,當WKWebView銷燬的時候,JS恰好執行了alert(),原生的 alert 彈窗可能彈不出來,completionHandler回調最後沒有被執行,致使crash;另外一種狀況是在WKWebView剛打開,JS就執行alert(),這個時候因爲 WKWebView所在的UIViewControllerpushpresent的動畫還沒有結束,alert框可能彈不出來,completionHandler最後沒有被執行,致使crash

解決方案:獲取當前window上最終的UIViewController,判斷UIViewController是否未被銷燬、UIViewController是否已經加載完成、動畫是否執行完畢。

2.另外一個crash發生在WKWebView退出前調用:
執行JS代碼的狀況下。WKWebView 退出並被釋放後致使completionHandler變成野指針,而此時 javaScript Core 還在執行JS代碼,待 javaScript Core 執行完畢後會調用completionHandler(),致使crash。這個crash只發生在iOS 8 系統上,參考Apple Open Source,在iOS9及之後系統蘋果已經修復了這個bug,主要是對completionHandler block作了copy(refer: https://trac.webkit.org/changeset/179160);對於iOS 8系統,能夠經過在completionHandlerretain WKWebView防止completionHandler被過早釋放。

解決方案是使用method swizzling hook了這個系統方法,在回調中對self進行了強引用來保證在執行completionHandler的時候self還在。

navigationBackItem

實現導航欄back item的方式有兩種。

  • 自定義導航欄

這個比較簡單,根據WebView是否能夠goback決定navigationBarButtonItems的個數和功能。

  • 使用系統默認的導航返回按鈕,相似於微信

難點在於咱們要獲取到點擊系統導航返回按鈕時的事件,而後進行一些處理。

點擊返回按鈕時,實際上調用了UINavigationControllernavigationBar:shouldPopItem方法,咱們可使用method swizzling hook住這個方法,在這個方法中經過調用代理方法的方式告訴WKWebView所在的UIViewController進行相應的處理。

UIProgressView

這個簡單,也很少說了。

Native與JS的交互
  • 攔截URL

WKWebViewdecidePolicyForNavigationAction代理方法中可對URL進行攔截,通常使用攔截URL的方式URL的格式以下:

scheme://host?paramKey=paramValue

通常狀況下scheme對應業務,host是業務對應的服務(method),?後面就是參數。

使用攔截URL的交互方式時,業務邏輯不復雜狀況下,JS調用Native沒什麼問題,但當業務邏輯複雜時,JS須要拿到Native處理好的回調數據的話,處理起來將十分麻煩。

而且使用攔截URL的交互方式,不利於從此JSNative的業務拓展。

  • 使用Bridge

WKWebViewJSNative經過Bridge交互提供了很是好的支持,咱們能夠經過ScriptMessageHandler來達成各類交互的目的。使用ScriptMessageHandler添加腳本的具體代碼在此很少贅述,你們可自行研究。重點說一下Bridge的腳本代碼。

如今關於Bridge的開源解決方案有不少,但基本都遵循一個模式,在注入的Bridge腳本代碼中,定義好供JS調用的方法名稱,該方法一般包括以下幾個參數:
1.要調用的native業務模塊名稱(有些有,有些沒有,若是項目中實施模塊化建議加上)。
2.要調用的native服務名稱(一般是方法名)。
3.傳遞給native的參數(也就是方法須要的參數)。
4.callbackJS調用native的方法後腳本須要調用的回調。

詳細來描述一下使用Bridge整個交互過程,從建立Bridge腳本到Bridge腳本執行callback
Bridge腳本下稱腳本。
1.腳本爲JS提供JavaScript語言的方法,該方法用來調用native方法,方法的4個參數如前所述。
2.在該方法中,會根據前述的部分參數生成一個惟一標識符,記爲identifier
3.在腳本中給全局對象(window)綁定一個字典屬性,key是步驟2中的identifiervaluecallback
4.調用messagehandlerpostMessage函數,將前述的參數和identifier 都發送給native(沒發callback,callback的做用主要就是步驟3)。
5.前端調用你的腳本中的代碼調用native的方法,具體代碼可參見Apple官方文檔。
5.native在自定義的MessageHandler對象的userContentController:didReceiveScriptMessage:代理方法中接收到JS傳過來的參數(記爲param)。獲取到了模塊名稱、服務名稱、參數、identifier等,額外的,須要建立幾個block,對應JS那邊的callback,好比JS那邊有個success callback,那麼在native就要有一個success block,而建立的這些block,咱們會賦值給前面說的那個param裏面,那麼如今,這個param有以下幾個值:

targetName(模塊名稱)
actionName(服務名稱)
identifier(經過該屬性最後咱們能夠找到js的callback)
success block
failure block
progress block
上面這些參數基本上已經夠了,若是須要擴展就本身加吧

那麼這些block裏面的操做主要是什麼呢?block封裝了WKWebViewevaluateJavaScript操做,這個block最後能夠拿到native處理任務後的結果和identifier,而後把結果轉換爲json數據,經過identifier找到JS那邊的callback,而後把結果的json數據做爲callback的參數回傳給JS那邊。代碼以下:

NSString *resultDataString = [self jsonStringWithData:resultDictionary];
    
NSString *callbackString = [NSString stringWithFormat:@"window.Callback('%@', '%@', '%@')", identifier, result, resultDataString];

[message.webView evaluateJavaScript:callbackString completionHandler:nil];

6.利用target-action機制,根據targetName實例化對象,根據actionName調用方法,並把參數(param)傳遞過去,目標對象將任務處理完成後,調用paramsuccess block, failure block, progress block,將任務處理的結果回傳給JS

  • 交互總結

不管是攔截URL仍是使用Bridge,最後調用native方法的機制都是利用target-action,使用target-action機制的緣由之一就是可減小類與類之間的耦合程度,減小硬編碼的同時有利於從此的業務擴展。

固然,若是你不喜歡target-action的方案,也能夠自行擴展。

攔截WKWebView的網絡請求

經過觀看WebKit的源碼能夠了解到WKWebView是支持攔截網絡請求的,可是WebKit沒有註冊須要攔截的scheme,因此咱們只能進行手動註冊了。

手動註冊須要調用WKWebView的私有api,註冊scheme的私有apiregisterSchemeForCustomProtocol:,註銷的私有apiunregisterSchemeForCustomProtocol:,有些同窗會考慮到在項目中使用私有api在審覈時會被蘋果爸爸打回,我這裏測試不會,若是你遇到了被打回的狀況,能夠把私有api拆分紅多個字符串,而後把多個字符串拼接在一塊兒。

因此攔截WKWebView網絡請求的步驟是:
(1)自定義NSURLProtocol,用來處理攔截到的網絡請求。
(2)利用系統提供的NSURLProtocol註冊(1)中自定義的NSURLProtocol
(3)經過私有api註冊須要攔截的網絡請求的scheme
(4)在合適的時機註銷(3)中註冊的scheme

H5啓動性能優化

H5最讓人詬病的一點就是它的用戶體驗沒有native好,其實H5的交互效果(不包括複雜的動效)已經很是接近於native了,因此剩下的缺點整體來講就是關於WebView的渲染問題,咱們在寫native界面的時候,頁面一打開就能看到咱們建立的UI元素,可是遠程的H5不能,由於遠程H5的頁面元素都須要去服務器獲取,隨後通過渲染才能展現,過程大體以下:

H5啓動流程

因此,一個H5頁面徹底展現給用戶所須要的時間遠比native頁面長的多。

因此針對於移動端來講,優化H5啓動性能的點主要有兩個:
(1)優化WebView的啓動速度
(2)讓HTML/CSS/JavaScript文件下載的更快一些,也就是離線包方案。

(1)優化WebView的啓動速度

App打開的時候並不會初始化瀏覽器內核,當咱們建立一個WKWebView的時候,系統纔會初始化瀏覽器內核,也就是說,當咱們第一次用WebView打開H5的時候,H5的顯示時間須要加上瀏覽器內核啓動時間,因此優化點就在於優化瀏覽器內核啓動時間。

不少解決方案是初始化一個單例WebView,讓這一個WebView全局可用,這樣打開每一個H5的時候用的都是同一個WebView對象,工做原理有點接近PC端瀏覽器,這樣作的缺點就是若是這個WebView由於某些緣由致使異常終止以後,再用這個WebView打開H5可能會產生一些意料以外的問題,因此,這裏推薦使用另一種解決方案。

另一種解決方案就是維護一個全局的WebView複用池,複用原理同UITableViewCell同樣,這裏不細講。若是一個WebView一直是正常工做的就放入複用池中,若是一個WebView由於某些緣由異常終止,那麼就把這個WebView從複用池中移除。

不管是哪一種複用方案,都會產生一個新問題,當咱們利用複用WebView打開一個新H5的時候,瀏覽器的瀏覽歷史記錄裏還保留着上一次打開的H5的痕跡,因此,咱們須要在複用時清除這個痕跡並讓頁面打開一個空白頁。

(2)使用離線包打包H5的靜態資源。

咱們經過一個遠程URL打開H5就能夠理解爲是在線打開的。

把一個H5HTML/CSS/JavaScript文件分別打包成靜態資源文件保存在服務器,這些保存在服務器的靜態資源文件就能夠理解爲是離線包,移動端能夠選擇一個合適的時機下載離線包,而後在本地解壓縮,當咱們打開一個H5的時候其實打開的是已經下載到本地的HTML文件,免去了在線拉取資源的過程,從而節省了時間。

H5頁面須要更新的時候,直接對離線包作增量更新能夠了。

更多細節可參考bang的這篇文章

基於WKWebView封裝的JXBWKWebView


1.內核決定了goback返回不刷新問題須要前端支持
2.支持natigationBackItem & navigationLeftItems
3.支持自定義rightBarButtonItem
4.支持進度條
5.提供cookie解決方案,首次本身加,後續的ajax請求自動加,302請求自動加
6.支持攔截WKWebView攔截網絡請求
7.支持POST請求
8.支持子類繼承
9.支持攔截URL的交互方式,支持自定義攔截URL操做。
10.提供nativeH5的交互解決方案,支持自定義MessageHandler操做。
11.提供H5秒開解決方案,server使用Go實現。
12.iOSAndroidJS提供統一的原生調用方式。

github地址:JXBWKWebView,若是以爲項目不錯能夠點個star支持一下,謝謝~

相關文章
相關標籤/搜索