三、攔截請求
1、支持NSURLProtocol 攔截
- 離線包方案關鍵之一:需要攔截請求,並返回本地資源;使用UIWebview時候,因為能通過NSURLProtocol可以攔截UIWebView的網絡請求,問題不大。
- WKWebview使用離線包方案,遇到最大問題:在WKWebView上無法直接利用NSURLProtocol攔截請求;這是因為WKWebview在獨立的進程(App進程之外)中執行網絡請求,請求數據不經過App進程。
- WKWebview上的解決辦法:使用私有API解決,iOS 11 之前使用
[WKBrowsingContextController registerSchemeForCustomProtocol:schema]
來注冊http/https,iOS之后可以通過 hook+(BOOL)handlesURLScheme
方式注冊http/https; - 但是一旦注冊http(s) scheme了,網絡請求將從
Network Process
發送到App Process
,然后被NSURLProtocol 攔截網絡請求。Network Process
將請求encode成一個Message,然后通過 IPC 發送給 App Process。出於性能的原因,encode的時候HTTPBody和HTTPBodyStream這兩個字段被丟棄掉了
2、WKURLSchemeHandler使用
-
iOS 11 中,Apple 為 WebKit framework 增加了
WKURLSchemeHandler
協議,用於加載自定義 URL Scheme。當WebKit
遇到無法識別的 URL時,會調用WKURLSchemeHandler
協議。該協議包括以下兩個必須實現的方法@protocol WKURLSchemeHandler <NSObject> //開始加載特定資源時調用 - (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask; //停止載特定資源時調用 - (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask; @end 復制代碼
-
使用
WKURLSchemeHandler
協議處理完任務后,調用WKURLSchemeTask
協議內方法加載資源。WKURLSchemeTask
協議屬性和方法如下:@protocol WKURLSchemeTask <NSObject> @property (nonatomic, readonly, copy) NSURLRequest *request; //設置當前任務的response。每個 task 至少調用一次該方法。如果嘗試在任務終止或完成后調用該方法,則會拋出異常。 - (void)didReceiveResponse:(NSURLResponse *)response; //設置接收到的數據。當接收到任務最后的 response 后,使用該方法發送數據。每次調用該方法時,新數據會拼接到先前收到的數據中。如果嘗試在發送 response 前,或任務完成、終止后調用該方法,則會引發異常。 - (void)didReceiveData:(NSData *)data; //將任務標記為成功完成。如果嘗試在發送 response 前,或將已完成、終止的任務標記為完成,則會引發異常。 - (void)didFinish; //將任務標記為失敗。如果嘗試將已完成、失敗,終止的任務標記為失敗,則會引發異常。 - (void)didFailWithError:(NSError *)error; 復制代碼
3、離線資源更新能力
- 很多項目中,為了優化Webview加載H5效果,使用了離線包方案,不僅需要攔截請求的能力,還需要更新離線資源的能力;比較有意思的是:ReactNative、Weex和小程序等跨端方案也需要依賴離線資源更新能力。
- 基於此,很多公司打造了離線資源打包、diff計算、動態下發,全量更新和增量更新等能力。以幫助更好地支持端上離線資源更新能力。
四、WKWebview中OC和JS通信
1、Message Handler
機制
-
iOS 2引入UIWebview,iOS7引入
JavaScriptCore
框架,它提供了 JS 代碼與原生代碼交互的能力;至此iOS7之后,UIWebview可以通過KVC方式獲取JSContext; 但是iOS 8引入的WKWebview
由於獨立在App進程之外,不能獲得JSContext,不能通過JSContext實現 JS 代碼與原生代碼通信。(iOS13開始,不再支持UIWebview) -
WKWebview提供了新的 JS 代碼 和 原生代碼通信的方式,這就是
Message Handler
這種機制,當JS執行window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
時,OC端被添加的ScriptMessageHandler
就會執行實現的WKScriptMessageHandler
協議中的方法@protocol WKScriptMessageHandler <NSObject> @required - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message; @end 復制代碼
2、傳統通信方式--URL攔截方式
- 基於URL攔截方式,實現JS和Native的交互,iOS非常經典的實現有: WebViewJavascriptBridge, 在很多業務中,選擇URL攔截方式也是個不錯的選擇;
五、WebView性能優化總結
1、加載性能優化思路
- 節省Webview初始化時間:提前初始化WebView,or 復用Webview對象
- 預先加載資源:
離線包方案
orlink prefetching方案
- 節約資源請求時間: DNS緩存、靜態資源存放在CDN上
- H5頁面優化:CSS、JavaScript、HTML優化等
2、禁止WKWebview中長按彈出UIMenuController
-
在WebView中,長按文字會使得WebView默認開始選擇文字;長按鏈接會彈出提示是否在新頁面打開。
-
解決方法:可以通過給body增加CSS來禁止這些默認規則。
// 禁止選擇CSS NSString *css = @"body{-webkit-user-select:none;-webkit-user-drag:none;}"; // CSS選中樣式取消 NSMutableString *javascript = [NSMutableString string]; [javascript appendString:@"var style = document.createElement('style');"]; [javascript appendString:@"style.type = 'text/css';"]; [javascript appendFormat:@"var cssContent = document.createTextNode('%@');", css]; [javascript appendString:@"style.appendChild(cssContent);"]; [javascript appendString:@"document.body.appendChild(style);"]; // javascript注入 WKUserScript *noneSelectScript = [[WKUserScript alloc] initWithSource:javascript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES]; WKUserContentController *userContentController = [[WKUserContentController alloc] init]; [userContentController addUserScript:noneSelectScript]; WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; configuration.userContentController = userContentController; // WKWebView 初始化 WKWebView *webView = [[WKWebView alloc] initWithFrame:frame configuration:configuration]; //... 復制代碼
3、點擊延遲優化
-
點擊延遲的原因:早期蘋果為了判斷移動端上的雙擊縮放事件而加的,在
touchend
和click
事件之間加300-350ms
的延遲,來判斷用戶到底是點擊還是雙擊。 -
優化方案1:使用
fastclick
庫,其原理是:在檢測到touchend
事件時,通過DOM自定義事件立即觸發模擬一個click事件,並把瀏覽器在300ms
之后的click事件阻止掉; -
優化方案2:禁用縮放,
WKWebView
上能解決延遲問題【不太適用於UIWebView
,但是在WKWebview上非常推薦】<meta name="viewport" content="user-scalable=no" /> 復制代碼
4、禁止Webview放大和縮小
-
方案1:實現
UIScrollViewDelegate
的viewForZoomingInScrollView:
的辦法- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{ return nil; } 復制代碼
-
方案2:HTML中的mata標簽加入
user-scalable = no
,若是原生js交互的話可采用注入js的方法更改meta。NSString *injectionJSString = @"var metaScript = document.createElement('meta');" "metaScript.name = 'viewport';" "metaScript.content=\"width=device-width, initial-scale=1.0,maximum-scale=1.0, minimum-scale=1.0, user-scalable=no\";" "document.head.appendChild(metaScript);"; WKUserScript *userScript = [[WKUserScript alloc] initWithSource:injectionJSString injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES]; WKUserContentController *userContentController = [[WKUserContentController alloc] init]; [wkuController addUserScript:userScript]; WKWebViewConfiguration *webViewConfig = [[WKWebViewConfiguration alloc] init]; webViewConfig.userContentController = userContentController; self.webView = [[WKWebView alloc] initWithFrame:webviewFrame configuration:webViewConfig]; 復制代碼
5、自動彈出鍵盤
-
H5頁面 focus 獲得焦點狀態下彈出鍵盤,UIWebView 中
keyboardDisplayRequiresUserAction
設置為 NO 就可以;(默認為YES,必須用戶點擊才可以彈出鍵盤) -
WKWebview沒有此屬性,需要通過hook私有API實現,代碼如下:
- (void)allowDisplayingKeyboardWithoutUserAction { Class class = NSClassFromString(@"WKContentView"); char * methodSignature = "_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:"; if (@available(iOS 11.3, *)) { methodSignature = "_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:"; } else if (@available(iOS 12.2, *)) { methodSignature = "_elementDidFocus:userIsInteracting:blurPreviousNode:changingActivityState:userObject:"; } if (@available(iOS 11.3, *)) { SEL selector = sel_getUid(methodSignature); Method method = class_getInstanceMethod(class, selector); IMP original = method_getImplementation(method); IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) { ((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4); }); method_setImplementation(method, override); } else { SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:"); Method method = class_getInstanceMethod(class, selector); IMP original = method_getImplementation(method); IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, id arg3) { ((void (*)(id, SEL, void*, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3); }); method_setImplementation(method, override); } } 復制代碼
6、WKWebview白屏優化
-
WKWebView是一個多進程組件,Network Loading以及UI Rendering在其它進程中執行,當WKWebView總體的內存占用比較大時,WebContent Process會crash,從而出現白屏現象。
-
解決辦法1:KVO監聽URL, 當URL為nil,重新reload
-
解決辦法2:在進程被終止回調中,重新reload
// 此方法適用iOS9.0以上 - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0){ //reload } 復制代碼
六、Webview安全
1、不打開WKWebview跨域開關
-
UIWebView 是允許跨域的,而 WKWebView(默認)不允許;但是WKWebView可以利用KVO的方式修改私有屬性實現跨域;
-
**個人建議:不要打開WKWebView跨域開關,因為打開后存在風險;**攻擊者可以利用App文件下載機制將惡意文件寫入沙盒內並誘導用戶打開;當用戶打開惡意文件后,惡意代碼可可通過
ajax
向file://
域發起請求,從而遠程獲取App沙盒內所有的本地數據。 -
2018年,國家信息安全漏洞庫(CNNVD)將WKWebView跨域漏洞定義為高危漏洞,為了更安全的WKWebview建議不打開;如果項目要送檢國家軟件評測中心的話,千萬千萬不能有如下打開跨域開關代碼;
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; [config.preferences setValue:@YES forKey:@"allowFileAccessFromFileURLs"]; if (@available(iOS 10.0, *)) { [config setValue:@YES forKey:@"allowUniversalAccessFromFileURLs"]; } 復制代碼
2、https解決WebView運營商劫持問題
- 因為WebView加載的頁面代碼是從Server獲取的,這些代碼將會很容易被中間環節所竊取或者修改,其中最主要的問題是運營商劫持問題。主要表現為:頁面被注入廣告、頁面被重定向等。
- 目前比較主流解決辦法:使用https可以防止頁面被劫持或者注入。
- https避不開中間人攻擊,兩種對抗中間人工具的方案:
- 對request 和 response 的敏感內容進行加密;建議AES加密,對AES的秘鑰管理建議:x小時后使用新的密鑰 or 對密鑰進行進一步處理(加密),增加攻擊者的破解成本。
- 客戶端處理證書校驗: 將服務器提供的SSL/TLS證書內置到APP客戶端中,當客戶端發起請求時,通過比對內置的證書和服務器端證書的內容,以確定這個連接的合法性。
- 某些H5業務中,采用了 對request 和 response 的敏感內容進行加密 這種保護方式;大部分H5業務不需要關注這些,用https就夠了。
3、App內WebView打開第三方App能力和收
-
放:本質上,WKWebView限制H5頁面打開三方 APP 的能力,但是我們可以繞開這個限制,打開第三方App。
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{ // 判斷URL 中的 Scheme 或 host,然后通過 [[UIApplication sharedApplication] openURL:] 方法打開。 } 復制代碼
-
收:泛濫H5頁面打開App能力比如必然不是不好的,可以設置白名單機制,只允許打開友商的App。
七、其他
1、WKWebview的IP直連方案
- 在阿里雲上看到WKWebview中"IP直連"方案說明,具體可看:WebView業務場景“IP直連”方案說明
- 讓WKWebview支持IP直連,遇到的挑戰應該挺多的吧,畢竟攔截WKWebview的請求,將域名換成IP后,需要接管基於IP的網絡請求的發送、數據接收、頁面重定向、頁面解碼、Cookie、緩存等邏輯;
- 要hold住這個方案,需要具備較強的網絡 + OS Framework的代碼級掌控能力,難度系數非常高,收益多大,不敢確定;
2、WKWebview支持WebP展示
- WebP是Google開發的一種高效的圖片編碼格式,Android的Webview是天然支持的,如果想要在WKWebview上也支持,辦法也是有的;
- 解決方案是:
- 攔截Webp圖片資源請求
- 通過NSURLSession下載圖片數據
- 將WebP解碼成相應的格式(可以是JPG,也可使是PNG)
- 至此,WKWebview變相支持了WebP展示;如果想進一步優化,可以將轉換后的圖片緩存在內存 or 磁盤,當下次還要使用這個Webp圖片的時候,直接從緩存中獲取並返回;
- 使用WebP圖片格式可以省流量,至於是否要在WKWebview中支持WebP,結合業務實際情況看吧;
3、WKWebview常見加載網絡錯誤
當之無愧Top3
- NSURLErrorTimedOut錯誤(-1001) :連接超時遇到此類問題可以重試;
- NSURLErrorNetworkConnectionLost錯誤( -1005) :
The network connection was lost
, 原因可能是:當前這個https請求准備復用TCP連接的時,實際上連接已經被服務器斷開了,遇到此類問題可以重試; - NSURLErrorCannotFindHost錯誤(-1003):原因可能是:DNS劫持 or 運營商LDNS故障引起的問題。
4、獲取Webview所在的UIViewController
//給WKWebview增加個分類,利用 響應鏈原理 獲取(宿主)ViewController
- (UIViewController *)hostViewController
{
UIResponder *responder = [self nextResponder];
if (!responder) {
return nil
}
while(responder && ![responder isKindOfClass:[UIViewController class]]) {
responder = [responder nextResponder];
}
return (UIViewController *)responder;
}
復制代碼
作者:南華Coder
鏈接:https://juejin.cn/post/6844904089294209038
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。