WKWebview使用二三事


三、攔截請求

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對象
  • 預先加載資源離線包方案 or link 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、點擊延遲優化
  • 點擊延遲的原因:早期蘋果為了判斷移動端上的雙擊縮放事件而加的,在touchendclick事件之間加300-350ms的延遲,來判斷用戶到底是點擊還是雙擊

  • 優化方案1:使用fastclick庫,其原理是:在檢測到touchend事件時,通過DOM自定義事件立即觸發模擬一個click事件,並把瀏覽器在300ms之后的click事件阻止掉;

  • 優化方案2:禁用縮放, WKWebView上能解決延遲問題【不太適用於UIWebView,但是在WKWebview上非常推薦】

    <meta name="viewport" content="user-scalable=no" />
    復制代碼
4、禁止Webview放大和縮小
  • 方案1:實現UIScrollViewDelegateviewForZoomingInScrollView:的辦法

    - (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文件下載機制將惡意文件寫入沙盒內並誘導用戶打開;當用戶打開惡意文件后,惡意代碼可可通過ajaxfile://域發起請求,從而遠程獲取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"];
    }
    復制代碼
  • 更多請參考: CNNVD 關於iOS平台WebView組件跨域漏洞情況的通報

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
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM