整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          淺談WKWebView使用、JS的交互

          淺談WKWebView使用、JS的交互


          WKWebView是iOS8 出來的瀏覽器控件,用來取代UIWebView.對于WKWebView與UIWebView的對比特點,這里就不過多的敘述,都算是老生常談的問題了,網上的說明也很多.近來在做Web端,需要植入移動端,并且做JS交互工作.以前寫過的JavaScript:淺談iOS與H5的交互-JavaScriptCore框架是用于UIWebView.在WKWebView并不適用了,自己做的過程中遇到一些坑,在這里總結一下,做一下記錄.

          WKWebView加載本地 html文件


          現在的項目要求就是使用存儲在本地的html文件 js文件 css文件 img圖片等文件.我使用HBuilder創建了一個Demo工程.里面包含了基本的文件以及圖片資源.如下所示.然后拖到Xcode工程中.

          然后我們把整個Html工程文件夾導入工程中.

          WKWebView 在iOS 9 有新的加載本地方法- (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL API_AVAILABLE(macosx(10.11), ios(9.0));.但是我看了網上有一些博客說- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;并不能加載本地html文件.其實兩者都能加載.只是需要把路徑寫對.第一個方法就不過多敘述了.網上有很多的博客.這里我就用第二個方法來看一下如何加載.

          NSString *path=[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"index.html"];

          NSURLRequest *request=[NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]];

          [_mainWebView loadRequest: request];

          • 1
          • 2
          • 3

          運行效果圖如下.

          Xcode工程中 html文件 加載js文件 css文件 img文件


          上面我們加載了html.可是css樣式 .img文件和js文件都沒有加載出來.那么我們該如何解決呢?(html原始加載圖如下所示.)

          對于css文件img文件 ,js文件我們只需要把html文件中的文件夾路徑刪除即可.如下所示.

          ???????? 注意:這里是使用的- (WKNavigation *)loadRequest:(NSURLRequest *)request;方式加載的網頁.所有如上設置即可.如果使用的是loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL或者loadHTMLString:(NSString *)string baseURL:(NSURL *)baseURL;都需要設置本地資源文件所對應的路徑! 我們只需要把資源文件所對應的路徑設置給readAccessURL 和 baseURL即可(). 例如下面所示.

          //假定都在工程根目錄之下.

          NSURL *baseURL=[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];

          [_mainWebView loadHTMLString:[self loadHtmlString] baseURL:baseURL];

          • 1
          • 2
          • 3

          對于js文件,有時候我們發現我們就算如上刪除了js文件夾的路徑依然不能使用.這是為什么呢?.這里就要去檢測查看工程的設置,是否把js文件當成一個可編譯文件使用了.我們需要把它移動到資源文件中.這樣就能正常加載了.如下圖所示.

          WKWebView中警告窗 確認面板 輸入框的顯示


          相比于UIWebView,WKWebView對警告窗 確認面板 輸入框并不能直接顯示.是通過WKUIDelegate代理方法收到對應的回調方法.如下所示.

          //接收到警告面板

          - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;

          //接收到確認面板

          - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler;

          //接收到輸入框

          - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler;

          • 1
          • 2
          • 3
          • 4
          • 5
          • 6

          我們只需要對上面的回調方法進行實現即可實現顯示警告窗 確認面板 輸入框等需求.

          WKWebView中OC方法調用JS方法


          在WKWebView OC方法調用JS方法方法比較簡單.我們只需要使用如下方法即可.

          - (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;

          • 1

          看一下我寫的例子.在index.js中定義一個方法.方法內容為彈出一個警告框.代碼如下所示.

          function alertAction(message) {

          alert(message);

          }

          • 1
          • 2
          • 3

          然后在ViewController控制器中定義一個Button.定義它的點擊方法.代碼如下所示.

          - (UIButton *)alertButton{

          if (_alertButton==nil) {

          _alertButton=[[UIButton alloc] initWithFrame:CGRectMake(KMainWidth*0.2, KMainHeight - 60, KMainWidth * 0.6, 40)];

          _alertButton.backgroundColor=[UIColor colorWithRed:250/255.0 green:204/255.0 blue:96/255.0 alpha:1.0];

          _alertButton.layer.cornerRadius=6.0f;

          _alertButton.layer.masksToBounds=YES;

          _alertButton.titleLabel.font=[UIFont systemFontOfSize:16];

          [_alertButton setTitle:@"彈出彈窗" forState:UIControlStateNormal];

          [_alertButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];

          [_alertButton addTarget:self action:@selector(alertButtonAction) forControlEvents:UIControlEventTouchUpInside];

          }

          return _alertButton;

          }

          - (void)alertButtonAction{

          [self.mainWebView evaluateJavaScript:@"alertAction('OC調用JS警告窗方法')" completionHandler:^(id _Nullable item, NSError * _Nullable error) {

          NSLog(@"alert");

          }];

          }

          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
          • 13
          • 14
          • 15
          • 16
          • 17
          • 18
          • 19
          • 20
          • 21
          • 22

          因為彈窗不能直接顯示.所以我們實現了對應的代理方法.如下所示.

          //接收到警告面板

          - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{

          UIAlertController *alert=[UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert];

          UIAlertAction *action=[UIAlertAction actionWithTitle:@"ok" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {

          completionHandler();//此處的completionHandler()就是調用JS方法時,`evaluateJavaScript`方法中的completionHandler

          }];

          [alert addAction:action];

          [self presentViewController:alert animated:YES completion:nil];

          }

          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10

          這時候我們點擊按鈕就會調用對應的JS方法了.相比于JavaScriptCore框架還是非常簡單的.效果圖如下所示.

          WKWebView中JS方法調用OC方法


          上一個模塊我們看到了OC方法調用JS方法.那么JS方法調用OC方法呢?我們也只需要幾步就可以完成調用.

          第一步.我們需要在WKWebView創建的過程中初始化添加ScriptMessageHandler.(命名自定例如:currentCookies.如下所示)

          WKWebViewConfiguration *configuration=[[WKWebViewConfiguration alloc] init];

          WKUserContentController *userController=[[WKUserContentController alloc] init];

          configuration.userContentController=userController;

          _mainWebView=[[WKWebView alloc] initWithFrame:CGRectMake(0, 0, KMainWidth, KMainHeight) configuration:configuration];

          [userController addScriptMessageHandler:self name:@"currentCookies"];

          • 1
          • 2
          • 3
          • 4
          • 5

          然后實現代理方法.監聽JS的回調.為了查看效果,我們仍然使用彈窗的形式展示我們的回調信息.代碼如下所示.

          //JS調用的OC回調方法

          - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{

          if ([message.name isEqualToString:@"currentCookies"]) {

          NSString *cookiesStr=message.body;

          NSLog(@"當前的cookie為: %@", cookiesStr);

          UIAlertController *alert=[UIAlertController alertControllerWithTitle:@"提示" message:@"JS調用的OC回調方法" preferredStyle:UIAlertControllerStyleAlert];

          UIAlertAction *action=[UIAlertAction actionWithTitle:@"ok" style:UIAlertActionStyleCancel handler:nil];

          [alert addAction:action];

          [self presentViewController:alert animated:YES completion:nil];

          }

          }

          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12

          然后在Html文件中添加點擊方法.

          <div id="button_div" onclick="buttonDivAction()">

          點擊調用OC方法

          </div>

          • 1
          • 2
          • 3

          緊接著.在js文件中實現點擊事件.這時候要注意的是ScriptMessageHandler的名稱要與上面定義的一致.

          function buttonDivAction() {

          window.webkit.messageHandlers.currentCookies.postMessage({

          "body": "buttonActionMessage"

          });

          }

          • 1
          • 2
          • 3
          • 4
          • 5

          然后我們點擊網頁中對應的div就會出現對應的彈窗.效果圖如下所示.

          并且控制臺打印信息如下所示.

          總結


          相比于UIWebView,WKWebView確實更加的方便快捷.本篇博客就到這里了.如果有任何問題,歡迎在評論區回復騷棟.感謝觀看,最后還是附上本篇博客的Demo傳送門.

          eb安全策略

          web安全對iOS開發者來說重要嗎?重要!APP中通常會使用很多web頁面,例如廣告、登錄流程、閃屏,或者需要使用跨平臺功能的時候。你可能在頁面中僅僅一部分使用web,也可以整個頁面都是webView,甚至做一個web app。因此web安全對于app來說非常重要。

          來自web的安全攻擊有以下幾種:

          • 跨域攻擊
          • 預測執行攻擊
          • 窗口控制攻擊

          本文將針對這三種攻擊類型,給出安全防御措施。

          安全傳輸

          網絡傳輸相信大家都很熟悉了,安全的傳輸能夠保證接收到的數據來自可信任的站點,并且在傳輸工程中不會被篡改。安全傳輸是其它安全措施的基礎,采取的措施包括:

          • HTTPS和WSS(webSocket Secure)
          • Http Strict Transport Security(HSTS)遵循HTTPS安全協議的web只能訪問同樣遵循HTTPS安全協議的內容,不能訪問遵循HTTP不安全協議的內容。
          • Upgrade-Insecure-Requests 請求頭中添加這一項表示web支持更加安全的升級機制,服務器可以重定向到這個站點的安全版本。
          • 使用cookie確保安全,cookie只能被安全的傳輸,例如千萬不要把cookie放到粘貼板上
          • 在app的info.plist文件中

          Allow Arbitrary Loads in Web Content 這個開關一定要置為 NO!

          跨域封鎖

          web的內容可以來自任何站點,例如,webView上的一張圖片可以來自任何服務器,也可以從任意服務器上加載一個腳本或iframe。需要注意的是要當心來自其它服務器的資源。跨域的保護已經有20多年的歷史,并且形成了基本原則--同源策略:只有和頁面來源相同的腳本才會被該頁面執行。例如iframe來自不同的域名,同源策略不允許加載這個iframe。僅僅靠同源策略還是不夠的,還需要采取其它的防御措施。

          1. Subresource Integrity

          服務器可能會發生異常導致下發錯誤的資源使得web發生crash,但是開發者通常是知道所要請求哪個資源的,在腳本里面增加一個檢查簽名。如果簽名匹配則認為是下發了正確的資源,如果不匹配仍然可以正常工作,此時嘗試從頁面的資源里查找或者從自己的服務器重新加載。這樣做雖然降低了性能,但是提升了安全性。

          <script src="https://cdn.example/framework.js"
           integrity="sha256-8WqyJLuWKRB...oZkCnxQbWwJVw=">
          </script>
          window.framwork || // reload from own domain
          

          2. Content Security Policy

          HTTP response:
          :status:200
          Content-Security-Policy:
           default 'self'; // No inline
           script-src cdn.example;
           frame-src social.example;
           frame-ancestors news.example;
          

          HTTP response的Header里面,default設置成自己,默認只能加載同源的資源;script-src和frame-src 分別指定可信任的腳本和iframe的來源;frame-ancestor設置成news.example,指定只有news.example可以iframe我們的web。

          另外不使用inline屬性的腳本也是一種防御措施,不使用inline腳本,只從文件加載腳本,這么做分離了邏輯和文件,更加安全。

          3. HttpOnly cookies

          HTTPOnly cookies作為一種安全措施,已經有至少15年的使用歷史。在這之前script通過document.cookie這個強大的api能拿到文檔的cookie,留下安全隱患。HTTPOnly cookies能夠阻止這種情況,只允許HTTP請求訪問cookie,禁止使用script訪問cookie。它的使用方式很簡單,只需要在HTTP response的Header里面加上HttpOnly這一項,如下

          HTTP response:
          :status:200
          Set-Cookie:
           auth=abc...123; HttpOnly;
          

          4. SameSite cookies

          在HTTP response的Header里面將SameSite cookies這一項設置為Strict,那么將不允許把cookie從一個域名發送到另一個域名。例如其他人的web里面嵌入了我們的web,如果我們的服務器HTTP response的Header里面SameSite cookies=Strict,那么其他人將無法使用他的cookie來訪問我們的服務器。

          HTTP response:
          :status:200
          Set-Cookie:
           auth=abc...123; HttpOnly;
           SameSite=strict
          

          5. Cross-Origin-Resource-Policy

          Cross-Origin-Resource-Policy是推出的新功能。之前web可以加載任意web中的資源,例如圖片或者script。在HTTP response的Header里面將Cross-Origin-Resource-Policy這一項設置為Same,將不允許別人的web向我們的服務器請求圖片或者script,但是我們自己的web可以。

          HTTP response:
          :status:200
          Cross-Origin-Resource-Policy:Same
          

          6. Cross-Origin-Window-Policy

          Cross-Origin-Window-Policy也是新推出的功能。之前通過window.open這個強大的api,其他人的web可以在新窗口中打開我們域名下的web,通過一些手段可以修改我們的web,導航到攻擊者指定的頁面。在HTTP response的Header里面將Cross-Origin-Resource-Policy這一項設置為Deny,將阻止其他人修改我們web中的內容,當然別人仍然還是可以打開我們的web。Cross-Origin-Resource-Policy適用于希望使用post message 進行窗口間通信,但是不想讓別人控制我們自己web內容的情況。

          HTTP response:
          :status:200
          Cross-Origin-Window-Policy:Deny
          

          常見的web攻擊及防御手段

          Cross-Origin Attacks

          • Cross-Site Scripting
          • Compromised CDN
          • Cross-Site Request Forgeries

          Cross-Site Scripting

          例如我們的web里面有一個文本框,用戶可以輸入文字,如下圖。假如攻擊者注入了這么一段腳本,如果沒有采取防御措施,那么我們web的cookie就會被盜取。

          在HTTP response的Header中添加HTTPOnly這一項,就能阻止腳本訪問文檔的cookie,從而防御跨域腳本攻擊。

          另外一種防御手段是Content-Security-Policy,如下

          HTTP response:
          :status:200
          Content-Security-Policy:
           default-src 'self'; // No inline
          

          Content-Security-Policy能保證拒絕加載外部來源的腳本,并且不使用inline屬性的腳本,只從文件中加載腳本。

          Compromised CDN

          例如我們的web需要從某個外部資源裝載一個framework,攻擊者可能攔截這個請求,并把它重定向到自己的攻擊腳本上,如下圖

          使用Content-Security-Policy中script-src這個屬性可以指定信任的腳本來源,并且在引用資源的時候指定來源和校驗簽名,如下

          在HTTP response中:

          HTTP response:
          :status:200
          Content-Security-Policy:
           default-src 'self'; 
           script-src cdn.example;
          

          在HTML中:

          <script src="https://cdn.example/framework.js"
           integrity="sha256-8WqyJLuWKRB...oZkCnxQbWwJVw=">
          </script>
          window.framwork || // reload from own domain
          

          3. Cross-Site Request Forgeries

          攻擊者可能在自己的web中嵌入我們的web,然后向我們的服務器發起一個偽造的網絡請求(使用的是攻擊者網站的cookie),如下圖

          如果采取了防御措施,將HTTP response的Header里面的SameSite設置為strict,那么就會禁止攻擊者網站的cookie發動到我們的服務器上面,如下

          HTTP response:
          :status:200
          Set-Cookie:
           auth=abc...123; SameSite=strict
          

          2. Speculative execution attacks (Spectre)

          防御措施有:

          • WKWebView
          • Content Security Policy
          • HttpOnly cookies
          • SameSite cookies
          • Cross-Origin-Resource-Policy

          Speculative execution 的定義:預測執行類似于批量執行條件判斷語句,例如計算機大量執行"x是否會造成數組array越界"這條指令,就能推測出這個數組的長度,進一步推測出這個數組在內存緩沖區中的地址邊界。利用緩沖區溢出這種攻擊手段,可以向web中注入攻擊腳本。當x超過數組邊界的時候,本來應該執行越界的error回調,但是確取出了攻擊腳本并執行,造成數據泄露。顯然只靠同源策略是無法防御這種攻擊的,因為攻擊腳本和文檔處在同一個域名下,并且在同一個線程中。

          防御預測執行攻擊的方法是確保web內容和其他iframe(例如攻擊腳本)處在不同的線程中

          WKWebView

          以Safari app為例,WKWebView會單獨分離出一個NetWork線程用于處理添加cookie等邏輯,而且每個網頁處在不同的線程當中,所以evil網頁是無法通過預測執行攻擊手段攻擊我們的頁面。而且因為NetWork線程也是獨立的,所以evil網頁也無法通過預測執行攻擊手段拿到重要數據,例如cookie。

          如果使用UIWebView,所有的web包括NetWork線程都在app的同一個線程中,所以是無法防御預測執行攻擊手段的。

          Content security policy

          Content security policy的封鎖功能是處于Network線程中,和web線程是分離的,因此可以防御預測執行攻擊手段。

          例如web要加載一個廣告iframe,但是這個廣告iframe被重定向到了一個攻擊腳本,如果使用了Content security policy,如下,因為攻擊腳本不在信任的frame-src里面,所以會禁止加載。還有一種情況,攻擊者的web引入了我們的web,因為設置了frame-ancestors為none,所以會禁止攻擊者網站引入我們的web,從而防御攻擊。

          HTTP response:
          :status:200
          Content-Security-Policy:
           default-src 'self'; 
           frame-src ad.example
           social.example
           frame-ancestors 'none'
          

          HttpOnly cookies 和 SameSite cookies

          HttpOnly cookies 和 SameSite cookies的封鎖功能也是處于Network線程中,和web線程是分離的,因此可以防御預測執行攻擊手段。HttpOnly cookies能夠禁止攻擊者通過腳本拿到cookie。SameSite cookies設為strict能夠禁止cookie從一個域發送到另一個域。

          Cross-Origin-Resource-Policy

          Cross-Origin-Resource-Policy的封鎖功能也是處于Network線程中,和web線程是分離的,因此可以防御預測執行攻擊手段。Cross-Origin-Resource-Policy設置成Same能禁止攻擊者的web加載我們網站的資源。

          Window Control Attacks

          Cross-Origin-Window-Policy

          攻擊者的頁面可以通過window.open這個api在新的窗口打開我們的web,攻擊者趁我們不注意的時候,把我們的頁面導航到釣魚頁面,然后誘導用戶填寫用戶名和密碼,這樣就竊取到了用戶信息。把HTTP response Header里面的Cross-Origin-Window-Policy設置為Deny,能夠禁止攻擊者修改我們的web,這樣攻擊者就無法導航到釣魚頁面。

          總結

          每種安全措施防御的攻擊類型

          建議

          • 使用安全的網絡傳輸(例如https,wss)
          • 設置Cookies為HttpOnly和其它安全選項
          • 把UIWebView升級到WKWebView
          • 測試防御措施是否生效,web是否仍然能正常工作。安全措施都具有一定的限制功能,因此測試web是否能正常工作非常重要。例如Content-Securityp-Policy的script-src白名單里少些了一個允許的域名,那么這個域名下的web就無法正常使用了。

          App開發中,內嵌WebView始終占有著一席之地。它能以較低的成本實現Android、iOS和Web的復用,也可以冠冕堂皇的突破蘋果對熱更新的封鎖。

          然而便利性的同時,WebView的性能體驗卻備受質疑,導致很多客戶端中需要動態更新等頁面時不得不采用其他方案。

          以發展的眼光來看,功能的動態加載以及三端的融合將會是大趨勢。那么如何克服WebView固有的問題呢?我們將從性能、內存消耗、體驗、安全幾個維度,來系統的分析客戶端默認WebView的問題,以及對應的優化方案。

          性能

          對于WebView的性能,給人最直觀的莫過于:打開速度比native慢。

          是的,當我們打開一個WebView頁面,頁面往往會慢吞吞的loading很久,若干秒后才出現你所需要看到的頁面。

          這是為什么呢?

          對于一個普通用戶來講,打開一個WebView通常會經歷以下幾個階段:

          1. 交互無反饋

          2. 到達新的頁面,頁面白屏

          3. 頁面基本框架出現,但是沒有數據;頁面處于loading狀態

          4. 出現所需的數據

          如果從程序上觀察,WebView啟動過程大概分為以下幾個階段:

          如何縮短這些過程的時間,就成了優化WebView性能的關鍵。

          接下來我們逐一分析各個階段的耗時情況,以及需要注意的優化點。

          WebView初始化

          當App首次打開時,默認是并不初始化瀏覽器內核的;只有當創建WebView實例的時候,才會創建WebView的基礎框架。

          所以與瀏覽器不同,App中打開WebView的第一步并不是建立連接,而是啟動瀏覽器內核

          我們來分析一下這段耗時到底需要多久。

          分析

          針對WebView的初始化時間,我們可以定義兩個指標:

          • 首次初始化時間:客戶端冷啟動后,第一次打開WebView,從開始創建WebView到開始建立網絡連接之間的時間。

          • 二次初始化時間:在打開過WebView后,退出WebView,再重新打開WebView,從開始創建WebView到開始建立網絡連接之間的時間。

          測試數據:

          測試系統1: iOS模擬器,Titans 10.0.7

          測試系統2: OPPO R829T Android 4.2.2

          測試方式:測試10次取平均值

          測試App:美團外賣

          單位:ms


          首次初始化時間二次初始化時間
          iOS(UIWebView)306.5676.43
          iOS(WKWebView)763.26457.25
          Android192.79 *142.53

          *Android外賣客戶端啟動后會在后臺開啟WebView進程,故并不是完全新建WebView時間。

          這意味著什么呢?

          作為前端工程師,統計了無數次的頁面打開時間,都是以網絡連接開始作為起點的。

          很遺憾的通知您:WebView中用戶體驗到的打開時間需要再增加70~700ms。

          于是我們找到了“為什么WebView總是很慢”的原因之一:

          • 在瀏覽器中,我們輸入地址時(甚至在之前),瀏覽器就可以開始加載頁面。

          • 而在客戶端中,客戶端需要先花費時間初始化WebView完成后,才開始加載。

          而這段時間,由于WebView還不存在,所有后續的過程是完全阻塞的。可以這樣形容WebView初始化過程:

          那么有哪些解決辦法呢?

          怎么優化

          由于這段過程發生在native的代碼中,單純靠前端代碼是無法優化的;大部分的方案都是前端和客戶端協作完成,以下是幾個業界采用過的方案。

          全局WebView

          方法:

          • 在客戶端剛啟動時,就初始化一個全局的WebView待用,并隱藏;

          • 當用戶訪問了WebView時,直接使用這個WebView加載對應網頁,并展示。

          這種方法可以比較有效的減少WebView在App中的首次打開時間。當用戶訪問頁面時,不需要初始化WebView的時間。

          當然這也帶來了一些問題,包括:

          • 額外的內存消耗。

          • 頁面間跳轉需要清空上一個頁面的痕跡,更容易內存泄露。

          【參考東軟專利 - 加載網頁的方法及裝置 CN106250434A】

          客戶端代理數據請求

          方法:

          • 在客戶端初始化WebView的同時,直接由native開始網絡請求數據;

          • 當頁面初始化完成后,向native獲取其代理請求的數據。

          此方法雖然不能減小WebView初始化時間,但數據請求和WebView初始化可以并行進行,總體的頁面加載時間就縮短了;縮短總體的頁面加載時間:

          【參考騰訊分享:70%以上業務由H5開發,手機QQ Hybrid 的架構如何優化演進?】

          還有其他各種優化的方式,不再一一列舉,總結起來都是圍繞兩點:

          1. 在使用前預先初始化好WebView,從而減小耗時。

          2. 在初始化的同時,通過Native來完成一些網絡請求等過程,使得WebView初始化不是完全的阻塞后續過程。

          建立連接/服務器處理

          在頁面請求的數據返回之前,主要有以下過程耗費時間。

          • DNS

          • connection

          • 服務器處理

          分析

          以下為美團中活動頁面的鏈接時間統計:

          統計: 美團的活動頁面

          內容值: n%分位值(ms)


          DNSconnection獲取首字節
          50%1.371172
          90%60360541

          優化

          這些時間都是發生在網頁加載之前,但這并不意味著無法優化,有以下幾種方法。

          DNS采用和客戶端API相同的域名

          DNS會在系統級別進行緩存,對于WebView的地址,如果使用的域名與native的API相同,則可以直接使用緩存的DNS而不用再發起請求圖片。

          以美團為例,美團的客戶端請求域名主要位于api.meituan.com,然而內嵌的WebView主要位于 i.meituan.com。

          當我們初次打開App時:

          • 客戶端首次打開都會請求api.meituan.com,其DNS將會被系統緩存。

          • 然而當打開WebView的時候,由于請求了不同的域名,需要重新獲取i.meituan.com的IP。

          根據上面的統計,至少10%的用戶打開WebView時耗費了60ms在DNS上面,如果WebView的域名與App的API域名統一,則可以讓WebView的DNS時間全部達到1.3ms的量級

          靜態資源同理,最好與客戶端的資源域名保持一致。

          同步渲染采用chunk編碼

          同步渲染時如果后端請求時間過長,可以考慮采用chunk編碼,將數據放在最后,并優先將靜態內容flush。對于傳統的后端渲染頁面,往往都是使用的【瀏覽器】--> 【Web API】 --> 【業務 API】的加載模式,其中后端時間就指的是Web API的處理時間了。在這里Web API一般有兩個作用:

          1. 確定靜態資源的版本。

          2. 根據用戶的請求,去業務API獲取數據。

          而一般確定靜態資源的版本往往是直接讀取代碼版本,基本無耗時;而主要的后端時間都花費在了業務API請求上面。

          那么怎么優化利用這段時間呢?

          在HTTP協議中,我們可以在header中設置 transfer-encoding:chunked 使得頁面可以分塊輸出。如果合理設計頁面,讓head部分都是確定的靜態資源版本相關內容,而body部分是業務數據相關內容,那么我們可以在用戶請求的時候,首先將Web API可以確定的部分先輸出給瀏覽器,然后等API完全獲取后,再將API數據傳輸給瀏覽器。

          下圖可以直觀的看出分chunk輸出和一起輸出的區別:

          • 如果采用普通方式輸出頁面,則頁面會在服務器請求完所有API并處理完成后開始傳輸。瀏覽器要在后端所有API都加載完成后才能開始解析。

          • 如果采用chunk-encoding: chunked,并優先將頁面的靜態部分輸出;然后處理API請求,并最終返回頁面,可以讓后端的API請求和前端的資源加載同時進行。

          • 兩者的總共后端時間并沒有區別,但是可以提升首字節速度,從而讓前端加載資源和后端加載API不互相阻塞。

          頁面框架渲染

          頁面在解析到足夠多的節點,且所有CSS都加載完成后進行首屏渲染。在此之前,頁面保持白屏;在頁面完全下載并解析完成之前,頁面處于不完整展示狀態。

          分析

          我們以一個美團的活動頁面作為樣例:

          測試頁面:http://i.meituan.com/firework/meituanxianshifengqiang

          在Mac上面,模擬4G情況

          頁面樣式:

          測試得到的時間耗費如下:

          表1


          階段時間大小備注
          DOM下載58ms29.5?KB4G網絡
          DOM解析12.5ms198?KB根據估算,在手機上慢2~5倍不等
          CSS請求+下載58ms11.7?KB4G網絡(包含鏈接時間,CDN)
          CSS解析2.89ms54.1?KB根據估算,在手機上慢2~5倍不等
          渲染23ms1361節點根據估算,在手機上慢2~5倍不等
          繪制4.1ms
          根據估算,在手機上慢2~5倍不等
          合成0.23ms
          GPU處理

          同時,對HTML的加載時間進行分析,可以得到如下時間點。

          表2


          指標時間計算方法
          HTML加載完成時間218performance.timing.responseEnd - performance.timing.fetchStart
          HTML解析完成時間330performance.timing.domInteractive - performance.timing.fetchStart

          這意味著什么呢?

          對于表1

          可以看到,隨著在網絡優良的情況下,Dom的解析所占耗時比例還是不算低的,對于低端機器更甚。Layout時間也是首屏前耗時的大頭,據猜測這與頁面使用了rem作為單位有關(待進一步分析)。

          對于表2,我們可以發現一個問題

          一般來說HTML在開始接收到返回數據的時候就開始解析HTML并構建DOM樹。如果沒有JS(JavaScript)阻塞的話一般會相繼完成。然而,在這里時間相差了90ms……也就是說,解析被阻塞了。

          進一步分析可以發現,頁面的header部分有這樣的代碼:

          .....

          通常情況下,上面代碼的link部分和script部分如果單獨出現,都不會阻塞頁面的解析:

          • CSS不會阻止頁面繼續向下繼續。

          • 內聯的JS很快執行完成,然后繼續解析文檔。

          然而,當這兩部分同時出現的時候,問題就來了。

          • CSS加載阻塞了下面的一段內聯JS的執行,而被阻塞的內聯JS則阻塞了HTML的解析。

          通常情況下,CSS不會阻塞HTML的解析,但如果CSS后面有JS,則會阻塞JS的執行直到CSS加載完成(即便JS是內聯的腳本),從而間接阻塞HTML的解析。

          優化

          在頁面框架加載這一部分,能夠優化的點參照雅虎14條就夠了;但注意不要犯錯,一個小小的內聯JS放錯位置也會讓性能下降很多。

          1. CSS的加載會在HTML解析到CSS的標簽時開始,所以CSS的標簽要盡量靠前。

          2. 但是,CSS鏈接下面不能有任何的JS標簽(包括很簡單的內聯JS),否則會阻塞HTML的解析。

          3. 如果必須要在頭部增加內聯腳本,一定要放在CSS標簽之前。

          JS加載

          對于大型的網站來說,在此我們先提出幾個問題:

          • 將全部JS代碼打成一個包,造成首次執行代碼過大怎么辦?

          • 將JS以細粒度打包,造成請求過多怎么辦?

          • 將JS按 "基礎庫" + "頁面代碼" 分別打包,要怎么界定什么是基礎代碼,什么是頁面代碼;不同頁面用的基礎代碼不一致怎么辦?

          • 單一文件的少量代碼改的是否會導致緩存失效?

          • 代碼模塊間有動態依賴,怎樣合并請求。

          關于這些問題的解決方案數量可能會比問題還多,而它們也各有優劣。

          具體分析太過復雜,鑒于篇幅原因在這里不做具體分析了。您可以期待我們的后續計劃:BPM(瀏覽器包管理)。

          JS解析、編譯、執行

          在PC互聯網時代,人們似乎都快忘記了JS的解析和執行還需要消耗時間。確實,在幾年前網速還在用kb衡量的時代里,JS的解析時間在整個頁面的打開時間里只能算是九牛一毛。

          然而,隨著網速越來越快,而CPU的速度反而沒有提升(從PC到手機),JS的時間開銷就成為問題了。那么JS的編譯和解析,在當今的頁面上要消耗多少時間呢?

          分析

          我們用以下方式來檢驗JS代碼的解析/編譯和執行時間:

          <script>

          將測試代碼放入 【test code】 位置,然后在手機中執行;

          • 在t1~t2期間,JS代碼僅僅聲明了一個函數,主要時間會集中在解析和編譯過程;

          • 在t2~t3時間段內,執行test時時間主要為代碼的執行時間

          在首次啟動客戶端后,打開WebView的測試頁面,我們可以得到如下的結果:

          測試系統: iPhone6 iOS 10.2.1

          測試系統: OPPO R829T Android 4.2.2

          內容值: 編譯時間(ms)/執行時間(ms)

          系統Zepto.jsVue.jsReact.js + ReactDOM.js
          iOS5.2 / 812.8 / 16.113.7 / 43.3
          Android13 / 4043 / 12726 / 353

          當保持客戶端進行不關閉情況下,關閉WebView并重新訪問測試頁面,再次測試得到如下結果:

          系統Zepto.jsVue.jsReact.js + ReactDom.js
          iOS0.9 / 1.95 / 7.43.5 / 23
          Android5 / 917 / 1225 / 60

          執行時間指的是框架代碼加載的頁面的初始化時間,沒有任何業務的調用。

          這意味著什么

          經過測試可以得出以下結論:

          • 偏重的框架,例如React,僅僅初始化的時間就會達到50ms ~ 350ms,這在對性能敏感的業務中時比較不利的。

          • 在App的啟動周期內,統一域名下的代碼會被緩存編輯和初始化結果,重復調用性能較好。

          所以,在移動瀏覽器上,JS的解析和執行時間并不是不可忽略的。

          在低端安卓機上,(框架的初始化+異步數據請求+業務代碼執行)會遠高于幾KB網絡請求時間;高性能的Web網站需要仔細斟酌前端渲染帶來的性能問題。

          優化

          • 高性能要求頁面還是需要后端渲染。

          • React還是太重了,面向用戶寫系統需要謹慎考慮。

          • JS代碼的編譯和執行會有緩存,同App中網頁盡量統一框架。

          WebView性能優化總結

          一個加載網頁的過程中,native、網絡、后端處理、CPU都會參與,各自都有必要的工作和依賴關系;讓他們相互并行處理而不是相互阻塞才可以讓網頁加載更快:

          • WebView初始化慢,可以在初始化同時先請求數據,讓后端和網絡不要閑著。

          • 后端處理慢,可以讓服務器分trunk輸出,在后端計算的同時前端也加載網絡靜態資源。

          • 腳本執行慢,就讓腳本在最后運行,不阻塞頁面解析。

          • 同時,合理的預加載、預緩存可以讓加載速度的瓶頸更小。

          • WebView初始化慢,就隨時初始化好一個WebView待用。

          • DNS和鏈接慢,想辦法復用客戶端使用的域名和鏈接。

          • 腳本執行慢,可以把框架代碼拆分出來,在請求頁面之前就執行好。

          WebView內存消耗

          分析

          為了測試WebView會消耗多少內存,我們設計了如下的測試方案:

          1. 客戶端啟動后,記錄消耗的內存。

          2. 打開空頁面,記錄內存的上漲。

          3. 退出。

          4. 打開空頁面,記錄內存上漲。

          5. 退出。

          6. 打開加載了代碼的頁面,記錄內存的額外增加。

          得到如下測試結果:

          測試系統: iOS模擬器,Titans 10.0.7

          測試系統: OPPO R829T Android 4.2.2

          測試方式:測試10次取平均值


          首次打開增加內存二次打開增加內存加載KNB+VUE+靈犀
          iOS UIWebView31.1M5.52M2M
          iOS WKWebView1.95M1.6M2M
          Android32.2M6.62M1.7M

          WKWebView的內存消耗相比其他低了一個數量級,在此方面相當占優。

          UIWebView和Android的WebView在首次初始化時都要消耗大量內存,之后每次新建WebView會額外增加一些。

          UIWebView的內存占用不會在關閉WebView時主動回收,每次新開WebView都會消耗額外內存。

          相比于性能,對于內存的優化可以做的還是比較有限的。

          • WKWebView的內存占用優勢比較大(代價是初始化比較慢)。

          • 頁面內代碼消耗的內存相比與WebView系統的內存消耗相比可以說是很低。

          WebView體驗

          除了打開的速度,WebView通常體驗也沒有native的實現更好,我們可以找到以下幾個例子:

          長按選擇

          在WebView中,長按文字會使得WebView默認開始選擇文字;長按鏈接會彈出提示是否在新頁面打開。

          解決方法:可以通過給body增加CSS來禁止這些默認規則。

          點擊延遲

          在WebView中,click通常會有大約300ms的延遲(同時包括鏈接的點擊,表單的提交,控件的交互等任何用戶點擊行為)。

          唯一的例外是設置的meta:viewpoint為禁止縮放的Chrome(然而并不是Android默認的瀏覽器)。

          解決方法:使用fastclick一般可以解決這個問題。

          頁面滑動期間不渲染/執行

          在很多需求中會有一些吸頂的元素,例如導航條,購買按鈕等;當頁面滾動超出元素高度后,元素吸附在屏幕頂部。

          這個功能在PC和native中都能夠實現,然而在WebView中卻成了難題:

          在頁面滾動期間,Scroll Event不觸發

          不僅如此,WebView在滾動期間還有各種限定:

          • setTimeout和setInterval不觸發。

          • GIF動畫不播放。

          • 很多回調會延遲到頁面停止滾動之后。

          • background-position: fixed不支持。

          • 這些限制讓WebView在滾動期間很難有較好的體驗。

          這些限制大部分是不可突破的,但至少對于吸頂功能還是可以做一些支持:

          解決方法:

          • 在iOS上,使用position: sticky可以做到元素吸頂。

          • 在Android上,監聽touchmove事件可以在滑動期間做元素的position切換(慣性運動期間就無效了)。

          鍵盤形態有限

          WebView對鍵盤的控制能力很弱,無法直接調起或者隱藏鍵盤,而且鍵盤的確認文案是無法自定義的。

          我們以百度為例:

          當你打開百度搜索時,點擊【換行】就完成了輸入并開始了搜索。

          為什么是【換行】而不是【搜索】呢?

          當然不是bug……而是……臣妾做不到啊!

          解決方法:

          目前只能通過由與App通過橋協議的方式,由App代為喚起鍵盤(但是實際操作過于復雜)。

          crash

          通常WebView并不能直接接觸到底層的API,因此比較穩定;但仍然有使用不當造成整個App崩潰的情況。

          目前發現的案例包括:

          • 使用過大的圖片(2M)

          • 不正常使用WebGL

          WebView安全

          WebView被運營商劫持、注入問題

          由于WebView加載的頁面代碼是從服務器動態獲取的,這些代碼將會很容易被中間環節所竊取或者修改,其中最主要的問題出自地方運營商(浙江尤其明顯)和一些WiFi。

          我們監測到的問題包括:

          • 無視通信規則強制緩存頁面。

          • header被篡改。

          • 頁面被注入廣告。

          • 頁面被重定向。

          • 頁面被重定向并重新iframe到新頁面,框架嵌入廣告。

          • HTTPS請求被攔截。

          • DNS劫持。

          這些問題輕則影響用戶體驗,重則泄露數據,或影響公司信譽。

          針對頁面注入的行為,有一些解決方案:

          使用CSP(Content Security Policy)

          CSP可以有效的攔截頁面中的非白名單資源,而且兼容性較好。在美團移動版的使用中,能夠阻止大部分的頁面內容注入。

          但在使用中還是存在以下問題:

          • 由于業務的需要,通常inline腳本還是在白名單中,會導致完全依賴內聯的頁面代碼注入可以通過檢測。

          • 如果注入的內容是純HTML+CSS的內容,則CSP無能為力。

          • 無法解決頁面被劫持的問題。

          • 會帶來額外的一些維護成本。

          總體來說CSP是一個行之有效的防注入方案,但是如果對于安全要求更高的網站,這些還不夠。

          HTTPS

          HTTPS可以防止頁面被劫持或者注入,然而其副作用也是明顯的,網絡傳輸的性能和成功率都會下降,而且HTTPS的頁面會要求頁面內所有引用的資源也是HTTPS的,對于大型網站其遷移成本并不算低。

          HTTPS的一個問題在于:一旦底層想要篡改或者劫持,會導致整個鏈接失效,頁面無法展示。這會帶來一個問題:本來頁面只是會被注入廣告,而且廣告會被CSP攔截,而采用了HTTPS后,整個網頁由于受到劫持完全無法展示。

          對于安全要求不高的靜態頁面,就需要權衡HTTPS帶來的利與弊了。

          App使用Socket代理請求

          如果HTTP請求容易被攔截,那么讓App將其轉換為一個Socket請求,并代理WebView的訪問也是一個辦法。

          通常不法運營商或者WiFi都只能攔截HTTP(S)請求,對于自定義的包內容則無法攔截,因此可以基本解決注入和劫持的問題。

          Socket代理請求也存在問題。

          • 首先,使用客戶端代理的頁面HTML請求將喪失邊下載邊解析的能力;根據前面所述,瀏覽器在HTML收到部分內容后就立刻開始解析,并加載解析出來的外鏈、圖片等,執行內聯的腳本……而目前WebView對外并沒有暴露這種流式的HTML接口,只能由客戶端完全下載好HTML后,注入到WebView中。因此其性能將會受到影響。

          • 其次,其技術問題也是較多的,例如對跳轉的處理,對緩存的處理,對CDN的處理等等……稍不留神就會埋下若干大坑。

          此外還有一些其他的辦法,例如頁面的MD5檢測,頁面靜態頁打包下載等等方式,具體如何選擇還要根據具體的場景抉擇。

          客戶端內打開第三方WebView

          一般來說,客戶端內的WebView都是可以通過客戶端的某個schema打開的,而要打開頁面的URL很多都并不寫在客戶端內,而是可以由URL中的參數傳遞過去的。

          那么,一旦此URL可以通過外界輸入自定義,那么就有可能在客戶端內部打開一個外部的網頁。

          例:作案過程

          • 某個App有個WebView,打開的schema為 appxx://web?url={weburl}。

          • App中有個掃碼的功能,可以掃描某個二維碼并打開對應的schema鏈接。

          • 某個壞人制作了一個二維碼并張貼到街上,內容符合 : appxx://web?url={some_hack_weburl}。

          • 用戶掃碼打開了some_hack_weburl。

          • 如果some_hack_weburl是一個高仿的登錄頁面,那么用戶將會很可能將用戶名密碼提交到其他網站。

          解決方法:在內嵌的WebView中應該限制允許打開的WebView的域名,并設置運行訪問的白名單。或者當用戶打開外部鏈接前給用戶強烈而明顯的提示。

          發展

          在一個客戶端內,native目前主要功能是提供高效而基礎的功能;內部的WebView則添加一些性能體驗要求不高但動態化要求高的能力。

          提高客戶端的動態能力,或者提高WebView的性能,都是提升App功能覆蓋的方式。

          而目前的各種框架,ReactNative、Week包括微信小程序,都是這個趨勢的嘗試。

          隨著技術的發展,WebView的性能、體驗和安全問題也將會逐漸的改善,在App中占有越來越多比重的同時,也將會為App開拓新的能力,為用戶帶來更優質的體驗。


          主站蜘蛛池模板: 久久久人妻精品无码一区| 午夜肉伦伦影院久久精品免费看国产一区二区三区 | 精品国产一区二区三区免费看| 国产成人高清视频一区二区| 日韩高清一区二区| 97久久精品无码一区二区天美| 亚洲日本一区二区三区在线| 日韩一区二区三区精品| 国产精品亚洲一区二区三区| 在线观看日本亚洲一区| 88国产精品视频一区二区三区| 春暖花开亚洲性无区一区二区 | 99久久精品费精品国产一区二区| 精品人妻无码一区二区色欲产成人| 久久精品国产一区二区三| 精品国产免费一区二区三区香蕉 | 一区二区三区伦理高清| 最新中文字幕一区| 伊人色综合网一区二区三区| 日韩精品一区二区三区中文精品| 亚洲国产美国国产综合一区二区| 手机看片福利一区二区三区| 精品国产不卡一区二区三区| 国产精品一区二区三区99| 亚洲AV永久无码精品一区二区国产| 精品中文字幕一区在线| 无码人妻AⅤ一区二区三区水密桃 无码欧精品亚洲日韩一区夜夜嗨 无码毛片一区二区三区中文字幕 无码毛片一区二区三区视频免费播放 | 亚洲一区中文字幕在线观看| 久久无码人妻一区二区三区 | 国产一区二区好的精华液 | 亚洲色大成网站www永久一区| 国产精品高清一区二区三区| 成人精品一区二区三区不卡免费看| 精品国产一区二区三区在线观看 | 亚洲无线码一区二区三区| 亚洲色大成网站www永久一区| 无码人妻一区二区三区免费n鬼沢| 中文字幕精品一区影音先锋| 中文乱码人妻系列一区二区 | 无码人妻久久一区二区三区 | 国产在线观看一区二区三区|