摘要:使用這種方案攔截的網絡請求造成的問題就是請求數據被清空,還是所為,看源碼主要看代碼中間那兩句注釋,大致的意思就是不會在進程間通信發送的。如何解決終極思路就是雖然的會在進程間通信時被丟棄,但不會。
github地址:JXBWKWebView,如果覺得項目不錯可以點個star支持一下,謝謝~
前言目前iOS系統已經更新到iOS11,大多數項目向下兼容最多兼容到iOS8,因此,在項目中對WebView組件進行重構再封裝時,打算直接舍棄UIWebView轉用WKWebView。
如果你目前正在網上瀏覽關于WKWebView的一些文章,相信你已經清楚了WKWebView的優點,也目睹了大家在使用WKWebView的過程中遇到的坑,而這篇文章,會對到目前為止大家遇到的關于WKWebView的問題給出詳細的解決方案,文章的最后,也會講述關于對WKWebView進行性能優化的方案。
解決的問題goback返回頁面不刷新
Cookie
POST請求失效
crash
navigationBackItem
進度條
Native與JS的交互
優化H5頁面啟動速度
入坑goback Api返回不刷新
在之前使用UIWebView時,調用goback后,頁面會刷新。使用WKWebView后,調用goback,即便調用reload方法,H5依然不會刷新。
原因是調用goback時,UIWebView會觸發onload事件,WKWebView不會觸發onload事件,如果前端依舊在onload事件中處理iOS的頁面返回事件,是處理不了的,解決方案是讓前端使用onpageshow事件監聽WKWebView的頁面goback事件。
前端代碼如下:
window.addEventListener("pageshow", function(event){ if(event.persisted){ location.reload(); } });
為了查看頁面是直接從服務器上載入還是從緩存中讀取,可以使用 PageTransitionEvent對象的persisted屬性來判斷。
如果頁面從瀏覽器的緩存中讀取該屬性返回ture,否則返回 false。然后在根據true或false在執行相應的頁面刷新動作或者直接ajax請求接口更新數據。
關于onload和onpageshow事件在safari和chrome上的區別如下:
. | 事件 | 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::encodePlatformData(Encoder& encoder, const ResourceRequest& resourceRequest) { RetainPtr 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 requestHTTPBody = adoptCF(CFURLRequestCopyHTTPRequestBody(requestToSerialize.get())); RetainPtr requestHTTPBodyStream = adoptCF(CFURLRequestCopyHTTPRequestBodyStream(requestToSerialize.get())); if (requestHTTPBody || requestHTTPBodyStream) { CFMutableURLRequestRef mutableRequest = CFURLRequestCreateMutableCopy(0, requestToSerialize.get()); requestToSerialize = adoptCF(mutableRequest); CFURLRequestSetHTTPRequestBody(mutableRequest, nil); CFURLRequestSetHTTPRequestBodyStream(mutableRequest, nil); } RetainPtr 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文件下載的更快一些,也就是離線包方案。
App打開的時候并不會初始化瀏覽器內核,當我們創建一個WKWebView的時候,系統才會初始化瀏覽器內核,也就是說,當我們第一次用WebView打開H5的時候,H5的顯示時間需要加上瀏覽器內核啟動時間,所以優化點就在于優化瀏覽器內核啟動時間。
很多解決方案是初始化一個單例WebView,讓這一個WebView全局可用,這樣打開每個H5的時候用的都是同一個WebView對象,工作原理有點接近PC端瀏覽器,這樣做的缺點就是如果這個WebView因為某些原因導致異常終止之后,再用這個WebView打開H5可能會產生一些意料之外的問題,所以,這里推薦使用另外一種解決方案。
另外一種解決方案就是維護一個全局的WebView復用池,復用原理同UITableViewCell一樣,這里不細講。如果一個WebView一直是正常工作的就放入復用池中,如果一個WebView因為某些原因異常終止,那么就把這個WebView從復用池中移除。
無論是哪種復用方案,都會產生一個新問題,當我們利用復用WebView打開一個新H5的時候,瀏覽器的瀏覽歷史記錄里還保留著上一次打開的H5的痕跡,所以,我們需要在復用時清除這個痕跡并讓頁面打開一個空白頁。
我們通過一個遠程URL打開H5就可以理解為是在線打開的。
把一個H5的HTML/CSS/JavaScript文件分別打包成靜態資源文件保存在服務器,這些保存在服務器的靜態資源文件就可以理解為是離線包,移動端可以選擇一個合適的時機下載離線包,然后在本地解壓縮,當我們打開一個H5的時候其實打開的是已經下載到本地的HTML文件,免去了在線拉取資源的過程,從而節省了時間。
當H5頁面需要更新的時候,直接對離線包做增量更新可以了。
更多細節可參考bang的這篇文章。
基于WKWebView封裝的JXBWKWebView1.內核決定了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支持一下,謝謝~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/98908.html
摘要:使用這種方案攔截的網絡請求造成的問題就是請求數據被清空,還是所為,看源碼主要看代碼中間那兩句注釋,大致的意思就是不會在進程間通信發送的。如何解決終極思路就是雖然的會在進程間通信時被丟棄,但不會。 github地址:JXBWKWebView,如果覺得項目不錯可以點個star支持一下,謝謝~ 前言 目前iOS系統已經更新到iOS11,大多數項目向下兼容最多兼容到iOS8,因此,在項目中對...
摘要:雖然蘋果官方提供了關于的與使用說明,但這并不能滿足開發者們的需求,各類復雜場景依舊讓我們焦頭爛額,而解決方案卻不易尋找。二源碼下載編譯及調試之前我們首先需要獲取一份蘋果官方的源碼。 一、前言移動互聯網時代,網頁依舊是內容展示的重要媒介,這離不開 WebKit 瀏覽內核技術的支持與發展。在 iOS 平臺下開發者們...
摘要:上面提到在安卓完全不需要像這樣大費周章的繞彎路,所以安卓可能就不需要這個自定義的,這樣又會導致面臨著與安卓差異化嚴重問題。前言 最早接觸離線包的概念要追溯到16年初,項目迎來大改版,其中重點項目之一就是離線包方案的制定與實施。離線包顧名思義就是將H5/CSS/JS和資源文件打包提前下發到App中,這樣App在加載網頁的時候實際上加載的是本地的文件,減少網絡請求來提高網頁的渲染速度,并實現動態...
閱讀 2664·2021-11-24 09:38
閱讀 1979·2019-08-30 15:53
閱讀 1234·2019-08-30 15:44
閱讀 3229·2019-08-30 14:10
閱讀 3579·2019-08-29 16:29
閱讀 1800·2019-08-29 16:23
閱讀 1099·2019-08-29 16:20
閱讀 1472·2019-08-29 11:13