github
地址:JXBWKWebView,若是以爲項目不錯能夠點個star支持一下,謝謝~前端
前言
目前iOS系統已經更新到iOS11,大多數項目向下兼容最多兼容到iOS8,所以,在項目中對WebView組件進行重構再封裝時,打算直接捨棄UIWebView
轉用WKWebView
。java
若是你目前正在網上瀏覽關於WKWebView
的一些文章,相信你已經清楚了WKWebView
的優勢,也目擊了你們在使用WKWebView
的過程當中遇到的坑,而這篇文章,會對到目前爲止你們遇到的關於WKWebView
的問題給出詳細的解決方案,文章的最後,也會講述關於對WKWebView
進行性能優化的方案。git
解決的問題
-
goback
返回頁面不刷新 Cookie
-
POST
請求失效 crash
navigationBackItem
- 進度條
-
Native
與JS
的交互 - 優化
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
。而後在根據true
或false
在執行相應的頁面刷新動做或者直接ajax
請求接口更新數據。json
關於onload
和onpageshow
事件在safari
和chrome
上的區別以下:api
. | 事件 | Chrome | Safari |
---|---|---|---|
第一次加載頁面 | onload | 觸發 | 觸發 |
第一次加載頁面 | onpageshow | 觸發 | 觸發 |
從其餘頁面返回 | onload | 觸發 | 不觸發 |
從其餘頁面返回 | onpageshow | 觸發 | 觸發 |
關於cookie
WKWebView
屬於webkit
框架,其將瀏覽器內核渲染進程提取出 App
主進程,由另一個進程進行管理,減小了至關一部分的性能損失,這也是性能上比UIWebView
優越的緣由之一。跨域
既然WKWebView
的工做進程獨立於App Process
以外,咱們暫且稱爲WK Process
(隨便起的)。
在使用AFN
進行網絡請求時,若是server
使用set-cookie
將cookie
寫入header
,AFN
接受到響應後會將cookie
保存到NSHTTPCookieStorage
,下次若是是同域的request url
,AFN
會將cookie
從NSHTTPCookieStorage
中取出而後做爲request header
的cookie
發送給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 header
的set-cookie
:
NSString *cookieString = [[response allHeaderFields] valueForKey:@"Set-Cookie"];
那麼,WKWebView
將cookie
存入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
請求(能夠理解域名發生變化,也就是說不一樣域)
,上面的解決方案就用不了了,這時就須要你在WKWebView
的decidePolicyForNavigationAction
代理方法中攔截URL
,判斷當前URL
與初次請求的URL
是否同域,若是不一樣域,在該代理方法中獲取到當前請求的request
對象並copy
出一個新的對象,經過addValue:forHeaderField:
方法將cookie
手動添加到header
中,而後讓WKWebView
使用loadRequest
從新加載這個copy
出來的新的request
對象。
問題就沒了嗎?NO
,上面的解決方法一樣有侷限,即只能解決後續的同域ajax
請求不加cookie
的問題。若是發生iframe
跨域請求,咱們攔截不到請求,因此也無法給請求的header
手動添加cookie
,WKWebView
只適合加載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
,只不過Apple
把http
和https
這兩個scheme
給過濾掉了,致使咱們攔截不到WKWebView
發送的網路請求。
所以,在咱們自定義NSURLProtocol
時,要經過使用私有api
來註冊一些scheme
,註冊scheme
的類名叫WKBrowsingContextController
,WKWebView
中有一個屬性叫browsingContextController
,就是這個類的對象。註冊的方法叫registerSchemeForCustomProtocol:
,知道這個私有api
,咱們就能夠經過target-action
的方式,註冊WKWebView
發起網絡請求時須要攔截的URL scheme
,此時註冊的scheme
至少要包括3種,分別是http
、https
、post
。
問題還沒玩,解決一個問題的同時每每伴隨另外一個問題的產生。
使用這種方案攔截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
不會在進程間通訊發送http
的body
。
由於WKWebView
屬於webkit
框架,所以WKWebView
的網絡請求、內容加載/渲染都是在WK Process
中進行,但NSURLProtocol
攔截請求還在App Process
,一旦註冊http(s) scheme
後,網絡請求將從獨立進程中發送到App Process
,這樣自定義的NSURLProtocol
才能攔截到網絡請求,爲了提高進程間通訊效率,出於性能上的考慮,Apple
會將request
的body
數據丟棄,由於body
數據(二進制類型)大小沒有限制,size
偏大的話就會對數據傳輸效率有嚴重影響進而影響到攔截請求時的操做及延時後續的網絡請求,所以,Apple
在進行進程間通訊時會把post
請求的body
丟棄。
如何解決?
終極思路就是雖然http
的body
會在進程間通訊時被丟棄,但header
不會。
所以,解決問題步驟以下:
-
WKWebView
在loadRequest
前對request
對象進行一些處理,這個request
對象咱們記爲old request
。
1.記下old request
的scheme
和NSData
類型的http body
。
2.獲取當前old request
的URL
,替換URL
的scheme
爲post
(這也是咱們爲何要在前面使用NSURLProtocol
註冊post scheme
的緣由),並根據這個替換好的URL
從新生成一個新的NSMutableURLRequest
對象,這個對象記爲new request
。
3.給new request
的header
賦值,把步驟1中獲取的scheme
和http body
手動添加到這個new request
的header
中,若是這個post
請求須要附帶cookie
的話,你也要把cookie
從old request
中拿出來放到new request
的header
中。
4.讓WKWebView
加載這個new request
。
-
WKWebView
發送新的request
時(這個request url
的scheme
是post
),咱們能夠在自定義NSURLProtocol
中攔截到這個請求,執行以下步驟:
1.替換scheme
,此時的scheme
是post
,你須要把post scheme
替換成old request
的scheme
,這個字段咱們以前已經保存下來了。
2.替換scheme
後會生成一個新的URL
,根據這個新的URL
生成一個NSURLMutableRequest
對象,將以前保存的http body
、cookie
放到這個新的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
所在的UIViewController
的push
或present
的動畫還沒有結束,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
系統,能夠經過在completionHandler
裏retain WKWebView
防止completionHandler
被過早釋放。
解決方案是使用method swizzling hook
了這個系統方法,在回調中對self
進行了強引用來保證在執行completionHandler
的時候self
還在。
navigationBackItem
實現導航欄back item
的方式有兩種。
- 自定義導航欄
這個比較簡單,根據WebView
是否能夠goback
決定navigationBarButtonItems
的個數和功能。
- 使用系統默認的導航返回按鈕,相似於微信
難點在於咱們要獲取到點擊系統導航返回按鈕時的事件,而後進行一些處理。
點擊返回按鈕時,實際上調用了UINavigationController
的navigationBar:shouldPopItem
方法,咱們可使用method swizzling hook
住這個方法,在這個方法中經過調用代理方法的方式告訴WKWebView
所在的UIViewController
進行相應的處理。
UIProgressView
這個簡單,也很少說了。
Native與JS的交互
- 攔截URL
在WKWebView
的decidePolicyForNavigationAction
代理方法中可對URL
進行攔截,通常使用攔截URL
的方式URL
的格式以下:
scheme://host?paramKey=paramValue
通常狀況下scheme
對應業務,host
是業務對應的服務(method
),?
後面就是參數。
使用攔截URL
的交互方式時,業務邏輯不復雜狀況下,JS
調用Native
沒什麼問題,但當業務邏輯複雜時,JS
須要拿到Native
處理好的回調數據的話,處理起來將十分麻煩。
而且使用攔截URL
的交互方式,不利於從此JS
與Native
的業務拓展。
- 使用
Bridge
WKWebView
對JS
與Native
經過Bridge
交互提供了很是好的支持,咱們能夠經過ScriptMessageHandler
來達成各類交互的目的。使用ScriptMessageHandler
添加腳本的具體代碼在此很少贅述,你們可自行研究。重點說一下Bridge
的腳本代碼。
如今關於Bridge
的開源解決方案有不少,但基本都遵循一個模式,在注入的Bridge
腳本代碼中,定義好供JS
調用的方法名稱,該方法一般包括以下幾個參數:
1.要調用的native
業務模塊名稱(有些有,有些沒有,若是項目中實施模塊化建議加上)。
2.要調用的native
服務名稱(一般是方法名)。
3.傳遞給native
的參數(也就是方法須要的參數)。
4.callback
,JS
調用native
的方法後腳本須要調用的回調。
詳細來描述一下使用Bridge
整個交互過程,從建立Bridge
腳本到Bridge
腳本執行callback
:Bridge
腳本下稱腳本。
1.腳本爲JS
提供JavaScript
語言的方法,該方法用來調用native
方法,方法的4個參數如前所述。
2.在該方法中,會根據前述的部分參數生成一個惟一標識符,記爲identifier
。
3.在腳本中給全局對象(window
)綁定一個字典屬性,key
是步驟2中的identifier
,value
是callback
。
4.調用messagehandler
的postMessage
函數,將前述的參數和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
封裝了WKWebView
的evaluateJavaScript
操做,這個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
)傳遞過去,目標對象將任務處理完成後,調用param
的success block, failure block, progress block
,將任務處理的結果回傳給JS
。
- 交互總結
不管是攔截URL
仍是使用Bridge
,最後調用native
方法的機制都是利用target-action
,使用target-action
機制的緣由之一就是可減小類與類之間的耦合程度,減小硬編碼的同時有利於從此的業務擴展。
固然,若是你不喜歡target-action
的方案,也能夠自行擴展。
攔截WKWebView的網絡請求
經過觀看WebKit
的源碼能夠了解到WKWebView
是支持攔截網絡請求的,可是WebKit
沒有註冊須要攔截的scheme
,因此咱們只能進行手動註冊了。
手動註冊須要調用WKWebView
的私有api
,註冊scheme
的私有api
是registerSchemeForCustomProtocol:
,註銷的私有api
是unregisterSchemeForCustomProtocol:
,有些同窗會考慮到在項目中使用私有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
頁面徹底展現給用戶所須要的時間遠比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
就能夠理解爲是在線打開的。
把一個H5
的HTML/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.提供native
與H5
的交互解決方案,支持自定義MessageHandler
操做。
11.提供H5
秒開解決方案,server
使用Go
實現。
12.iOS
和Android
爲JS
提供統一的原生調用方式。
github
地址:JXBWKWebView,若是以爲項目不錯能夠點個star支持一下,謝謝~