Android Webview H5 秒開方案實現

玉剛說發表於2018-09-10

本文首發於微信公眾號「玉剛說」

原文連結:Android Webview H5 秒開方案實現

前言

現在許多app都嵌入了H5頁面, 然而WebView載入速度慢這個問題卻一直影響著使用者的體驗, 所以本文就如何提高H5頁面的載入速度展開討論。

問題原因

首先我們需要知道為什麼WebView的載入速度那麼慢。H5頁面的渲染速度其實主要取決於兩個

  1. js解析效率
    如果js檔案較多、解析比較複雜, 就會導致渲染速度較慢。或者手機的硬體效能比較差的話, 也會導致渲染速度比較慢。
  2. 頁面資源的下載
    一般載入一個H5頁面, 都會產生較多的網路請求, 如圖片、js檔案、css檔案等, 需要將這些資源都下載完成之後才能完成渲染, 這樣也會導致頁面渲染速度變慢

對於上面的第一點, 其實主要是由前端程式碼和手機硬體決定的, 因為我們這裡討論的是對於app的效能優化, 暫時不考慮, 所以我們可以從第二點做文章, 主要思路就是一些資原始檔都使用App本地資源, 而不需要從網路下載, 從而提高頁面的開啟速度。

程式碼實現

以載入玉剛說的renyugang.io/post/75這個頁面為例。

首先將一些資原始檔放在本地的assets目錄, 然後重寫WebViewClient的shouldInterceptRequest(WebView view, String url)和shouldInterceptRequest(WebView view, WebResourceRequest request)這兩個方法, 對訪問地址進行攔截, 當url地址命中本地配置的url時, 使用本地資源替代, 否則就使用網路上的資源。

YuGangShuoWebActivity:

mWebview.setWebViewClient(new WebViewClient() {
    // 設定不用系統瀏覽器開啟,直接顯示在當前Webview
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
      view.loadUrl(url);
      return true;
    }

    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
      // 如果命中本地資源, 使用本地資源替代
      if (mDataHelper.hasLocalResource(url)) {
          WebResourceResponse response =
                  mDataHelper.getReplacedWebResourceResponse(getApplicationContext(),
                          url);
          if (response != null) {
              return response;
          }
      }
      return super.shouldInterceptRequest(view, url);
    }

    @TargetApi(VERSION_CODES.LOLLIPOP)
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view,
          WebResourceRequest request) {
      String url = request.getUrl().toString();
      if (mDataHelper.hasLocalResource(url)) {
          WebResourceResponse response =
                  mDataHelper.getReplacedWebResourceResponse(getApplicationContext(),
                          url);
          if (response != null) {
              return response;
          }
      }
      return super.shouldInterceptRequest(view, request);
    }

}); 
複製程式碼

DataHelper是一個工具類, 程式碼如下:

public class DataHelper {

    private Map<String, String> mMap;

    public DataHelper({
        mMap = new HashMap<>();
        initData();
    }

    private void initData({
        String imageDir = "images/";
        String pngSuffix = ".png";
        mMap.put("http://renyugang.io/wp-content/themes/twentyseventeen/style.css?ver=4.9.8",
                "css/style.css");
        mMap.put("http://renyugang.io/wp-content/uploads/2018/06/cropped-ryg.png",
                imageDir + "cropped-ryg.png");
        ...
    }

    public boolean hasLocalResource(String url{
        return mMap.containsKey(url);
    }

    public WebResourceResponse getReplacedWebResourceResponse(Context context, String url{
        String localResourcePath = mMap.get(url);
        if (TextUtils.isEmpty(localResourcePath)) {
            return null;
        }
        InputStream is = null;
        try {
            is = context.getApplicationContext().getAssets().open(localResourcePath);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        String mimeType;
        if (url.contains("css")) {
            mimeType = "text/css";
        } else if (url.contains("jpg")) {
            mimeType = "image/jpeg";
        } else {
            mimeType = "image/png";
        }
        WebResourceResponse response = new WebResourceResponse(mimeType, "utf-8"is);
        return response;
    }


}
複製程式碼

我們抓包看一下修改前後的網路請求的對比。

優化前, 有n個實際發出的網路請求:
Android Webview H5 秒開方案實現

優化後, 只有一個實際發出的網路請求。並且為了和網路的資源圖片做區分, 我在兩張本地圖片中加了“本地”的水印, 能明顯看到這時候載入的是本地圖片:
Android Webview H5 秒開方案實現
另外再提一點, 對於WebViewClient的shouldInterceptRequest(WebView view, String url)和shouldInterceptRequest(WebView view, WebResourceRequest request)這兩個方法, 經本人親測, 重寫其中的任何一個都能生效, 後面一個shouldInterceptRequest(WebView view, WebResourceRequest request)一般是5.0以上的系統使用。我個人的建議是把這兩個方法都重寫了。

關於WebView的快取

我們再看一個有意思的現象, 在不配置本地資源的時候, 我們第一次開啟頁面, 產生了n多個請求。但是當我們退出後再次開啟這個頁面(沒有設定載入本地資源)的時候, 居然只發生了一次請求, 這現象與載入本地資源十分相似。
Android Webview H5 秒開方案實現
這是為什麼呢?
我們解除安裝app, 抓包, 再次開啟頁面, 以banner圖片請求的舉例。
Android Webview H5 秒開方案實現
我們觀察這個請求的response的headers中的引數, 注意到這麼幾個欄位:
Last-ModifiedETagExpiresCache-Control

  • Cache-Control
    例如Cache-Control:max-age=2592000, 表示快取時長為2592000秒, 也就是一個月30天的時間。如果30天內需要再次請求這個檔案,那麼瀏覽器不會發出請求,直接使用本地的快取的檔案。這是HTTP/1.1標準中的欄位。

  • Expires
    例如Expires:Tue,25 Sep 2018 07:17:34 GMT, 這表示這個檔案的過期時間是格林尼治時間2018年9月25日7點17分。因為我是北京時間2018年8月26日15點請求的, 所以可以看出也是差不多一個月有效期。在這個時間之前瀏覽器都不會再次發出請求去獲取這個檔案。Expires是HTTP/1.0中的欄位,如果客戶端和伺服器時間不同步會導致快取出現問題,因此才有了上面的Cache-Control。當它們同時出現時,Cache-Control優先順序更高。

  • Last-Modified
    標識檔案在伺服器上的最新更新時間, 下次請求時,如果檔案快取過期,瀏覽器通過If-Modified-Since欄位帶上這個時間,傳送給伺服器,由伺服器比較時間戳來判斷檔案是否有修改。如果沒有修改,伺服器返回304(未修改)告訴瀏覽器繼續使用快取;如果有修改,則返回200,同時返回最新的檔案。

  • Etag
    Etag的取值是一個對檔案進行標識的特徵字串, 在向伺服器查詢檔案是否有更新時,瀏覽器通過If-None-Match欄位把特徵字串傳送給伺服器,由伺服器和檔案最新特徵字串進行匹配,來判斷檔案是否有更新:沒有更新回包304,有更新回包200。Etag和Last-Modified可根據需求使用一個或兩個同時使用。兩個同時使用時,只要滿足基中一個條件,就認為檔案沒有更新。

常見用法是Cache-Control與Last-Modified一起使用, Expires與 Etag一起使用。

但是實際情況可能並不是這樣。

現在過了5分鐘, 我們再次開啟頁面, 觀察請求。
Android Webview H5 秒開方案實現
在上面這個請求中, 我們在request中沒有看到If-None-Match欄位, 說明Etag這個欄位沒有用到。但是在request中有If-Modified-Since這個欄位, 表示快取檔案的上次的修改日期, 是1984年, 表示當時從伺服器請求下來的檔案最後一次的修改時間是1984年, 而我們在response中看到Last-Modified欄位還是那個時間, 說明伺服器上的檔案沒有修改過, 所以返回了304(未修改), 而Cache-Control在這裡是300秒, 表示5分鐘就會過期, 而Expires在這裡雖然也出現了, 但是我們上面說過, 當Cache-Control和Expires同時出現時, Cache-Control的優先順序較高。

所以說, 大部分情況下, 我們其實看Cache-Control和Last-Modified欄位足矣。

好了, 話說回來, 現在我們知道為什麼會有之前提到的現象了, 是因為WebView的快取。

那麼如何才能使WebView支援這些快取協議呢?答案是不配置(使用預設的CacheMode), 或者手動設定

WebSettings webSettings = webView.getSettings();
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
複製程式碼

下面是5中快取模式的解釋:

  • LOAD_CACHE_ONLY: 不使用網路,只讀取本地快取資料。
  • LOAD_DEFAULT: 根據cache-control決定是否從網路上取資料。
  • LOAD_CACHE_NORMAL: API level 17中已經廢棄,從API level 11開始作用同LOAD_DEFAULT模式
  • LOAD_NO_CACHE: 不使用快取,只從網路獲取資料。
  • LOAD_CACHE_ELSE_NETWORK,只要本地有,無論是否過期,或者no-cache,都使用快取中的資料。本地沒有快取時才從網路上獲取。

所以我們一般設定為預設的快取模式就可以了。關於快取的配置, 主要還是靠web前端和後臺設定。

除了WebView自帶的快取, 還有Application Cache快取, Dom Storage快取, Web SQL Database快取, IndexedDB快取。但是剩下的幾種快取, 根據官方文件, AppCache已經不推薦使用了, 標準也不會再支援。而其他的幾種也不是檔案快取, 和我們今天討論的主題不符, 所以我也不再介紹了。有興趣可以看H5 快取機制淺析 移動端 Web 載入效能優化Android:手把手教你構建 全面的WebView 快取機制 & 資源載入方案

其他提升WebView速度的方案

WebView的初始化

本地Webview初始化都要不少時間, 首次初始化webview與第二次初始化不同,首次會比第二次慢很多。原因預計是webview首次初始化後,即使 webview 已經釋放,但一些webview 共用的全域性服務或資源物件仍沒有釋放,第二次初始化時不需要再生成這些物件從而變快。我們可以在Application預先初始化好WebView, 當第二次初始化WebView的時候速度就快多了, 或者直接將其拿來使用。

預載入資料

預載入資料就是在客戶端初始化WebView的同時,直接由native開始網路請求資料, 當頁面初始化完成後,向native獲取其代理請求的資料, 資料請求和WebView初始化可以並行進行,縮短總體的頁面載入時間。簡單來說就是配置一個預載入列表,在APP啟動或某些時機時提前去請求,這個預載入列表需要包含所需H5模組的頁面和資源, 客戶端可以接管所有請求的快取,不走webview預設快取邏輯, 自行實現快取機制, 原理其實就是攔截WebViewClient的那兩個shouldInterceptRequest方法。

離線包

離線包的意思就是將H5的頁面和資源進行打包後下發到客戶端,並由客戶端直接解壓到本地儲存中。優點是由於其本地化,首屏載入速度快,使用者體驗更為接近原生, 可以不依賴網路,離線執行, 缺點就是開發流程/更新機制複雜化, 需要客戶端、甚至服務端的共同協作。這裡我以Hybrid App技術解析 -- 實戰篇中提到的思路為例子供大家參考。

資源:

  • H5: 每個程式碼包都有一個唯一且遞增的版本號;
  • Native: 提供包下載且解壓資原始檔到對應目錄
  • 服務端: 提供一個介面,可以獲取線上最新程式碼包的版本號和下載地址。

流程:

  • 前端更新程式碼打包後按版本號上傳至指定的伺服器上;
  • 每次開啟頁面時,H5請求介面獲取線上最新程式碼包版本號,並與本地包進行版本號比對,當線上的版本號大於本地包版本號時,呼叫原生下載離線包
  • 客戶端直接去線上地址下載最新的程式碼包,並解壓替換到當前目錄檔案。

關於離線包的機制需要注意的問題還很多, 本文肯定無法照顧完全, 大家可以參考移動H5首屏秒開優化方案探討美團大眾點評 Hybrid 化建設《移動端本地 H5 秒開方案探索與實現》這幾篇文章看看。

一些開源方案

CacheWebView
這個庫的介紹連結在這裡my.oschina.net/yale8848/bl…, 據作者說主要是為了解決Android自身快取空間太小(12M)的問題, 程式碼我簡單看了一下, 主要也是攔截這兩個方法:

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    public WebResourceResponse interceptRequest( WebResourceRequest request) {
        if (mInterceptor==null){
            return null;
        }
        return mInterceptor.interceptRequest(request);
    }

    @Override
    public WebResourceResponse interceptRequest(String url) {
        if (mInterceptor==null){
            return null;
        }
        return mInterceptor.interceptRequest(url);
    }
複製程式碼

然後使用Okhttp去下載資源, 同時給OkHttpClient配置了快取攔截器, 因為OkHttp能夠很好的支援快取, 這樣就突破了WebView快取空間太小和快取不可控的問題。

VasSonic
騰訊出品的一個輕量級的高效能的Hybrid框架,專注於提升頁面首屏載入速度,完美支援靜態直出頁面和動態直出頁面,相容離線包等方案。優點是效能好, 速度快, 大廠出品, 缺點是配置複雜, 同時需要前後端接入。VasSonic的程式碼我沒有看, 感興趣的可以看他們的VasSonic/wiki騰訊祭出大招VasSonic,讓你的H5頁面首屏秒開!

總結

怎樣提高WebView的載入速度其實涉及到的方面很多, 需要注意的細節也很多, 沒有辦法一概而論。大家需要按照公司的業務需要量體裁衣, 按需配置。

本文Demo
github.com/mundane7996…

參考:

Android:手把手教你構建 全面的WebView 快取機制 & 資源載入方案
WebView快取原理分析和應用
H5 和移動端 WebView 快取機制解析與實戰
騰訊祭出大招VasSonic,讓你的H5頁面首屏秒開!
《移動端本地 H5 秒開方案探索與實現》
移動 H5 首屏秒開優化方案探討
美團大眾點評 Hybrid 化建設
H5 快取機制淺析 移動端 Web 載入效能優化
QQ會員基於 Hybrid 的高質量 H5 架構實踐
從WebView快取聊到Http 的快取機制 | 掘金技術徵文
美團: WebView效能、體驗分析與優化

Android Webview H5 秒開方案實現
歡迎關注我的微信公眾號「玉剛說」,接收第一手技術乾貨

相關文章