HybridCache:一種簡單的native與webview共享快取的設計

LemonYang發表於2018-04-05

HybridCache簡而言之其實是一套native和webview共享快取的解決方案。不過在瞭解HybridCache的實現細節以及能夠解決的問題之前,先大概瞭解一下web開發中涉及到的快取機制

Web快取機制

實際上,web開發當中已經具備相當完善的快取機制,並且Android系統的WebView對這些已有的快取機制基本上都提供了完備的支援。

web的快取機制有以下兩大類:

  • 瀏覽器快取機制
  • web開發中的快取機制

瀏覽器快取機制

瀏覽器自身的快取機制是基於http協議層的Header中的資訊實現的

  • Cache-control && Expires

    這兩個欄位的作用是:接收響應時,瀏覽器決定檔案是否需要被快取;或者需要載入檔案時,瀏覽器決定是否需要發出請求

    Cache-control常見的值包括:no-cache、no-store、max-age等。其中max-age=xxx表示快取的內容將在 xxx 秒後失效, 這個選項只在HTTP 1.1可用, 並如果和Last-Modified一起使用時, 優先順序較高。

  • Last-Modified && ETag

    這兩個欄位的作用是:發起請求時,伺服器決定檔案是否需要更新。服務端響應瀏覽器的請求時會新增一個Last-Modified的頭部欄位,欄位內容表示請求的檔案最後的更改時間。而瀏覽器會在下一次請求通過If-Modified-Since頭部欄位將這個值返回給服務端,以決定是否需要更新檔案

這些技術都是協議層所定義的,在Android的webview當中我們可以通過配置決定是否採納這幾個協議的頭部屬性。設定如下:

webView.settings.cacheMode=WebSettings.LOAD_DEFAULT
// cacheMode的取值定義如下:
@IntDef({LOAD_DEFAULT, LOAD_NORMAL, LOAD_CACHE_ELSE_NETWORK, LOAD_NO_CACHE, LOAD_CACHE_ONLY})
@Retention(RetentionPolicy.SOURCE)
public @interface CacheMode {}
複製程式碼

web開發中的快取機制

  • Application Cache
  • Dom Storage 快取機制
  • Web SQL Database 快取機制
  • IndexedDB 快取機制
  • File System

關於以上這幾個web開發中的快取機制,可以參考這篇文章Android:手把手教你構建 全面的WebView 快取機制 & 資源載入方案

認識HybridCache

HybridCache旨在提供一種native和webview之間共享快取的解決方案,尤其是共享native中的圖片快取。在native開發中,我們廣泛的使用著各種圖片載入庫,比如:

這些存在native的圖片載入框架為我們提供了非常良好的圖片快取體驗。HybridCache的一種具體運用,就是把在webview中的圖片交由我們的native的圖片載入框架(或者是我們自己實現的檔案快取)進行快取,這樣的好處就是:

  • 能夠更持久的儲存webview中的圖片(而且不需要前端開發人員所關注)
  • 能夠更加統一app內的圖片快取
  • webview和native的快取貢獻,在某些適用的場景具備節省流量和加快載入速度的優點

當然圖片快取只是一個相當具體的運用,實際上HybridCache提供的是更為廣泛的webview資源載入攔截的功能,通過攔截webview中渲染網頁過程中各種資源(包括圖片、js檔案、css樣式檔案、html頁面檔案等)的下載,根據業務的場景考慮快取的策略,可以從app端提供webview的快取技術方案(不需要前端人員感知的)。

實現原理

Android的webview在載入網頁的時候,使用者能夠通過系統提供的API干預各個中間過程。而HybridCache要攔截的就是網頁資源請求的環節。這個過程,WebViewClient當中提供了以下兩個入口:

public class WebViewClient {

	// android5.0以上的版本加入
   public WebResourceResponse shouldInterceptRequest(WebView view,
            WebResourceRequest request) {
        return shouldInterceptRequest(view, request.getUrl().toString());
    }

	  @Deprecated
    public WebResourceResponse shouldInterceptRequest(WebView view,
            String url) {
        return null;
    }
}
複製程式碼

上面的兩個API是在呼叫了WebView#loadUrl()之後,請求網頁資源(包括html檔案、js檔案、css檔案以及圖片檔案)的時候回撥。關於這兩個API有幾個點需要注意:

  • 回撥不是發生在主執行緒,因此不能做一些處理UI的事情
  • 介面的返回值是同步的
  • WebResourceResponse這個返回值可以自行構造,其中關鍵的屬性主要是:代表資源內容的一個輸入流InputStream以及標記這個資源內容型別的mMimeType

只要在這兩個入口構造正確的WebResourceResponse物件,就可以替換預設的請求為我們提供的資源。因此,webview和native快取共享的方案就是通過這兩個入口,在每次請求資源的時候根據請求的URL/WebResourceRequest判斷是否存在本地的快取,並在快取存在的情況下將快取的輸入流返回,示意圖如下所示:

HybridCache:一種簡單的native與webview共享快取的設計

方案設計

先放上一張方案實現的設計類圖:

HybridCache:一種簡單的native與webview共享快取的設計

ps:這張類圖是一開始設計方案的時候畫的,後續經過了多次重構和調整,部分已經不盡一致,不過基本保持了核心的概念和結構

HybridCache的核心任務就是攔截資源請求,下載資源並快取資源,因此整個庫的設計就分為了下面三個核心點:

  • 請求攔截
  • 資源響應(下載/讀取快取)
  • 快取

資源請求攔截

參考okhttp攔截器的思想設計了WebResInterceptorChain兩個介面,定義了攔截的動作以及驅動攔截器的鏈條。實際上,這兩個介面都只是類庫內部可見。具體的實現是BaseInterceptorDefaultInterceptorChain兩個物件。

BaseInterceptor是攔截髮生和資源響應的核心物件,內部處理了包括尋找快取資源、下載資源和寫快取的基本邏輯。同時它是一個抽象類,子類只需要實現它並根據對應的資源請求定義是否參與攔截、以及選擇性的自定義配置下載和快取的行為即可。

DefaultInterceptorChain僅僅只是用於用於驅動攔截器鏈條的流轉,類庫內部可見

資源響應

資源響應有兩種情況:

  • 快取響應
  • 下載響應

當對應的資源快取不存在的時候,會直接觸發資源的下載。在類庫內部,會通過HttpConnectionDownloader直接建立一個HttpURLConnection進行資源的下載,獲得資源的檔案流。

同時參考代理模式,設計了邊讀邊寫的動作。即下載的資源流通過被封裝為一個WebResInputStreamWrapper物件後直接返回。WebResInputStreamWrapper繼承於InputStream,同時內部持有一個TempFileWriter的例項。在WebResInputStreamWrapper被瀏覽器讀取的同時,TempFileWriter會把對應的資源寫入到快取當中,實現邊讀邊寫

快取

CacheProvider定義了提供快取的實現的規範,可以根據實際的業務場景提供任意的快取實現方案。同時庫內部通過LruCache提供了簡單的檔案快取的實現SimpleCacheProvider。同時為了擴充共享圖片快取的實現,類庫還提供了一個基於fresco的圖片快取提供例項FrescoImageProvider

CacheKeyProvider使得業務可以根據實際的場景提供快取的key的生成策略。

關於方案的實現細節,可以關注我的GitHub倉庫HybridCache

接入使用

在圖片快取以及簡單的使用檔案快取資源這兩個場景上,方案已經提供了直接的實現,可以簡單的一鍵接入使用,總的接入步驟如下:

  1. 根據業務需要定義你的攔截器。你只需要繼承BaseInterceptor,並實現僅有的一個抽象方法即可。如果你需要圖片攔截器,可以直接使用類庫內部提供的ImageInterceptor
  2. 在定義攔截器的同時,你可以實現你的快取提供器,提供你的快取管理策略。預設的情況下會使用SimpleCacheProvider提供檔案快取
  3. 使用HybridCacheManager#addCacheInterceptor()將攔截器新增都管理器中。
  4. 在初始化webview的時候,設定自定義的WebViewClient物件,並在其攔截資源請求的入口方法中呼叫HybridCacheManager#interceptWebResRequest()方法

以上簡單的幾步即可擁有native和webview共享快取的功能。具體的例項可以參考GitHub倉庫中的demo。

你可能會遇到的坑

在使用webview的時候,你可能會遇到一些坑

  • 頁面當中資源包含http和https兩種請求

    如果你載入的頁面以及頁面請求的資源包好了http和https兩種請求,那麼你有可能會出現部分資源無法載入的情況。這是因為在Android5.0之後,webview預設禁止在一個頁面當中包含兩種協議請求。這時候你需要新增這樣的設定:setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW)

  • 載入的頁面部分的資源請求出錯(比如404)沒有回撥

    正如前面提及的,在Android6.0以後,你可以通過WebViewClient#onReceivedHttpError()接收到資源請求出錯的回撥,但是在此之前是沒有這個api的,同時另外一個APIWebViewClient#onReceivedError()的回撥是在整個頁面不可達等情況才會回撥,而不會因為資源請求問題而響應,具體可以參考文件備註。

    在使用HybridCache資源攔截之後,你可以通過設定BaseInterceptor#setOnErrorListener(onErrorListener)感知到資源載入出錯的情況

  • 快取key不同導致快取不能共享

目前這個方案已經在我們的專案中實際使用。你可以在我的GitHub倉庫HybridCache中看到簡單的例項.歡迎大家表達對這個方案的設計的看法和改進意見,謝謝。

相關文章