WebView 快取原理分析和應用

UncleChen's Blog發表於2017-05-15

一、背景

現在的App開發,或多或少都會用到Hybrid模式,到了WebView這邊,經常會載入一些js檔案(例如和WebView用來Native通訊的bridge.js),而這些js檔案不會經常發生變化,所以我們希望js在WebView裡面載入一次之後,如果js沒有發生變化,下次就不用再發起網路請求去載入,從而減少流量和資源的佔用。那麼有什麼方式可以達到這個目的呢?先得從WebView的快取原理入手。

二、WebView的快取型別

WebView主要包括兩類快取,一類是瀏覽器自帶的網頁資料快取,這是所有的瀏覽器都支援的、由HTTP協議定義的快取;另一類是H5快取,這是由web頁面的開發者設定的,H5快取主要包括了App Cache、DOM Storage、Local Storage、Web SQL Database 儲存機制等,這裡我們主要介紹App Cache來快取js檔案。

三、瀏覽器自帶的網頁資料快取

1.工作原理

瀏覽器快取機制是通過HTTP協議Header裡的Cache-Control(或Expires)和Last-Modified(或 Etag)等欄位來控制檔案快取的機制。關於這幾個欄位的作用和瀏覽器的快取更新機制,大家可以看看這兩篇文章(H5 快取機制淺析 移動端 Web 載入效能優化Android:手把手教你構建 WebView 的快取機制 & 資源預載入方案),裡面有詳細的介紹。下面從我實際應用的角度,介紹一下通常會在HTTP協議中遇到的Header。

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

  • Cache-Control:max-age=315360000,這表示快取時長為315360000秒。如果315360000秒內需要再次請求這個檔案,那麼瀏覽器不會發出請求,直接使用本地的快取的檔案。這是HTTP/1.1標準中的欄位。
  • Expires: Thu, 31 Dec 2037 23:55:55 GMT,這表示這個檔案的過期時間是2037年12月31日晚上23點55分55秒,在這個時間之前瀏覽器都不會再次發出請求去獲取這個檔案。這是HTTP/1.0中的欄位,如果客戶端和伺服器時間不同步會導致快取出現問題,因此才有了上面的Cache-Control,當它們同時出現在HTTP Response的Header中時,Cache-Control優先順序更高。

下面兩個欄位是發起請求時,伺服器決定檔案是否需要更新的欄位。

  • Last-Modified:Wed, 28 Sep 2016 09:24:35 GMT,這表示這個檔案最後的修改時間是2016年9月28日9點24分35秒。這個欄位對於瀏覽器來說,會在下次請求的時候,作為Request Header的If-Modified-Since欄位帶上。例如瀏覽器快取的檔案已經超過了Cache-Control(或者Expires),那麼需要載入這個檔案時,就會發出請求,請求的Header有一個欄位為If-Modified-Since:Wed, 28 Sep 2016 09:24:35 GMT,伺服器接收到請求後,會把檔案的Last-Modified時間和這個時間對比,如果時間沒變,那麼瀏覽器將返回304 Not Modified給瀏覽器,且content-length肯定是0個位元組。如果時間有變化,那麼伺服器會返回200 OK,並返回相應的內容給瀏覽器。
  • ETag:”57eb8c5c-129”,這是檔案的特徵串。功能同上面的Last-Modified是一樣的。只是在瀏覽器下次請求時,ETag是作為Request Header中的If-None-Match:"57eb8c5c-129"欄位傳到伺服器。伺服器和最新的檔案特徵串對比,如果相同那麼返回304 Not Modified,不同則返回200 OK。當ETag和Last-Modified同時出現時,任何一個欄位只要生效了,就認為檔案是沒有更新的。

2.WebView如何設定才能支援上面的協議

由上面的介紹可知,只要是個主流的、合格的瀏覽器,都應該能夠支援HTTP協議層面的這幾個欄位。這不是我們開發者可以修改的,也不是我們應該修改的配置。在Android上,我們的WebView也支援這幾個欄位。但是我們可以通過程式碼去設定WebView的Cache Mode,而使得協議生效或者無效。WebView有下面幾個Cache Mode:

  • 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,都使用快取中的資料。本地沒有快取時才從網路上獲取。

設定WebView快取的Cache Mode示例程式碼如下:

WebSettings settings = webView.getSettings();
settings.setCacheMode(WebSettings.LOAD_DEFAULT);

網上很多人都說根據網路條件去選擇Cache Mode,當有網路時,設定為LOAD_DEFAULT,當沒有網路時設定為LOAD_CACHE_ELSE_NETWORK。但是在我的業務中,js檔案的更新都是非覆蓋式的更新,也就是時候每次改變js檔案的時候,檔案的url地址一定會發生變化,所以我希望瀏覽器能夠快取下來js,並且一直使用它,那麼我就給它只設定為LOAD_CACHE_ELSE_NETWORK。當然如果你要是可以改js的cdn伺服器的Cache-Control欄位,那也行啊,用LOAD_DEFAULT就ok了。至於檔案是應該採用覆蓋式or非覆蓋式的更新,不是我今天要討論的內容,在web前端領域,這是一個可以聊聊的topic。

關於iOS的WebView,我同事在實際測試的時候竟然發現,控制檔案快取的Response Header是Expires欄位。。而且iOS無法針對整個WebView設定Cache Mode,只能針對每一個URLRequest去設定。。後續有機會要學習一下iOS那塊的情況。

3.在手機裡面的儲存路徑

瀏覽器預設快取下來的檔案是怎麼被儲存到了哪裡呢?這個問題在接觸到WebView以來,就一直是一個謎題。這次由於工作的需要,我特意root了兩臺手機,一臺紅米1(Android 4.4)和一臺小米4c(Android 5.1),在root高系統版本(6.0和7.1)的兩臺Nexus都以失敗告終之後,我決定還是先看看4.4和5.1系統上,WebView自帶的快取存到了哪裡。

首先,不用思考就知道,這些檔案一定是在/data/data/包名/目錄下,在我之前的一篇部落格裡面提到過,這是每一個應用自己的內部儲存目錄。

接著,我們開啟終端,使用adb連線手機,然後按照下面命令操作一下。

// 1.先進入shell
adb shell
// 2.開啟root賬號 
su
// 3.修改資料夾許可權
chmod 777 data/data/你的應用包名/
// 4.修改子資料夾的許可權,因為Android命令列不支援向Linux那樣的-R命令實現遞迴式的chmod。。。
chmod 777 data/data/你的應用包名/*
// 5.所以如果你對應用目錄層級更深,你就要進一步地chmod。。。
chmod 777 data/data/你的應用包名/*/*
// 6.直到終端裡提示你說,no such file or directory時,說明chmod完了,所有的內部儲存裡面的資料夾和檔案都可以看到了,如果大家有更好的方法請一定告訴我,多謝了~
  • Android 4.4的目錄:/data/data/包名/app_webview/cache/,如下圖所示的第二個資料夾。

可能你注意到了,第一個資料夾是叫Application Cache,我們後面再說它。

  • Android 5.1的目錄:/data/data/包名/cache/org.chromium.android_webview/下面,如下圖所示。

但是在5.1系統上,/data/data/包名/app_webview/資料夾依然存在,只是4.4系統上面儲存WebView自帶快取的app_webview/cache資料夾不再存在了(注意下App Cache目錄還在),如下圖所示。

綜上所述,WebView自帶的瀏覽器協議支援的快取,在不同的系統版本上,位置是不一樣的。也許除了我root過的4.4、5.1以外,其他版本系統的WebView自帶快取還可能存在於不同的目錄裡面。

另外一個是關於快取檔案的儲存格式和索引格式,在不同的手機上可能也有差別,因為之前看到網上的人都說有叫webview.db或者webviewCache.db的檔案,這個檔案呢,還不是在app_webview/cache或者org.chromium.android_webview下面,而是在/data/data/包名/database/裡面。但是,我這兩臺root過的手機都沒有看到這種檔案,而且我把/data/data/包名/下面所有的db檔案都開啟看了,並沒有發現有儲存url記錄的table。。

實際上,以5.1系統為例,我看到了/data/data/包名/cache/org.chromium.android_webview/下面有叫index/index-dir/the-real-index的檔案,以及一堆名稱為md5+下劃線+數字的檔案,上面的圖中也可以看得到,這塊的原理仍然有些疑問,也希望專業的大神可以解答一下。

四、H5的快取

講完了WebView自帶的快取,下面講一下H5裡面的App Cache。這個Cache是由開發Web頁面的開發者控制的,而不是由Native去控制的,但是Native裡面的WebView也需要我們做一下設定才能支援H5的這個特性。

1.工作原理

寫Web頁面程式碼時,指定manifest屬性即可讓頁面使用App Cache。通常html頁面程式碼會這麼寫:

<html manifest="xxx.appcache">
</html>

xxx.appcache檔案用的是相對路徑,這時appcache檔案的路徑是和頁面一樣的。也可以使用的絕對路徑,但是域名要保持和頁面一致。

完整的xxx.appcache檔案一般包括了3個section,基本格式如下:

CACHE MANIFEST
# 2017-05-13 v1.0.0
/bridge.js

NETWORK:
*

FALLBACK:
/404.html
  • CACHE MANIFEST下面檔案就是要被瀏覽器快取的檔案
  • NETWORK下面的檔案就是要被載入的檔案
  • FALLBACK下面的檔案是目標頁面載入失敗時的顯示的頁面

AppCache工作的原理:當一個設定了manifest檔案的html頁面被載入時,CACHE MANIFEST指定的檔案就會被快取到瀏覽器的App Cache目錄下面。當下次載入這個頁面時,會首先應用通過manifest已經快取過的檔案,然後發起一個載入xxx.appcache檔案的請求到伺服器,如果xxx.appcache檔案沒有被修改過,那麼伺服器會返回304 Not Modified給到瀏覽器,如果xxx.appcache檔案被修改過,那麼伺服器會返回200 OK,並返回新的xxx.appcache檔案的內容給瀏覽器,瀏覽器收到之後,再把新的xxx.appcache檔案中指定的內容載入過來進行快取。

可以看到,AppCache快取需要在每次載入頁面時都發出一個xxx.appcache的請求去檢查manifest檔案是不是有更新(byte by byte)。根據這篇文章(H5 快取機制淺析 移動端 Web 載入效能優化)的介紹,AppCache有一些坑的地方,且官方已經不推薦使用了,但目前主流的瀏覽器依然是支援的。文章裡主要提到下面這些坑:

  • 要更新快取的檔案,需要更新包含它的 manifest 檔案,那怕只加一個空格。常用的方法,是修改 manifest 檔案註釋中的版本號。如:# 2012-02-21 v1.0.0
  • 被快取的檔案,瀏覽器是先使用,再通過檢查 manifest 檔案是否有更新來更新快取檔案。這樣快取檔案可能用的不是最新的版本。
  • 在更新快取過程中,如果有一個檔案更新失敗,則整個更新會失敗。
  • manifest 和引用它的HTML要在相同 HOST。
  • manifest 檔案中的檔案列表,如果是相對路徑,則是相對 manifest 檔案的相對路徑。
  • manifest 也有可能更新出錯,導致快取檔案更新失敗。
  • 沒有快取的資源在已經快取的 HTML 中不能載入,即使有網路。例如:[url=]http://appcache-demo.s3-website-us-east-1.amazonaws.com/without-network/[/url]
  • manifest 檔案本身不能被快取,且 manifest 檔案的更新使用的是瀏覽器快取機制。所以 manifest 檔案的 Cache-Control 快取時間不能設定太長。

2.WebView如何設定才能支援AppCache

WebView預設是沒有開啟AppCache支援的,需要新增下面這幾行程式碼來設定:

WebSettings webSettings = webView.getSettings();
webSettings.setAppCacheEnabled(true);
String cachePath = getApplicationContext().getCacheDir().getPath(); // 把內部私有快取目錄'/data/data/包名/cache/'作為WebView的AppCache的儲存路徑
webSettings.setAppCachePath(cachePath);
webSettings.setAppCacheMaxSize(5 * 1024 * 1024);

注意:WebSettings的setAppCacheEnabled和setAppCachePath都必須要呼叫才行。

3.儲存AppCache的路徑

按照Android SDK的API說明,setAppCachePath是可以用來設定AppCache路徑的,但是我實際測試發現,不管你怎麼設定這個路徑,設定到應用自己的內部私有目錄還是外部SD卡,都無法生效。AppCache快取檔案最終都會存到/data/data/包名/app_webview/cache/Application Cache這個資料夾下面,在上面的Android 4.4和5.1系統目錄截圖可以看得到,但是如果你不呼叫setAppCachePath方法,WebView將不會產生這個目錄。這裡有點讓我覺得奇怪,我猜測可能從某一個系統版本開始,為了快取檔案的完整性和安全性考慮,SDK實現的時候就吧AppCache快取目錄設定到了內部私有儲存。

五、總結

相同點

WebView自帶的快取和AppCache都是可以用來做檔案級別的快取的,基本上比較好地滿足對於非覆蓋式的js、css等檔案更新。

不同點

  • WebView自帶的快取是是協議層實現的(瀏覽器核心標準實現,開發者無法改變);而AppCache是應用層實現的。
  • WebView的快取目錄在不同系統上可能是不同的;而對於AppCache而言,AppCache的儲存路徑雖然有方法設定,但是最終都儲存到了一個固定的內部私有目錄下。
  • WebView自帶的快取可以在快取生效的時候不用再發HTTP請求;而AppCache一定會發出一個manifest檔案的請求。
  • WebView自帶的快取可以通過設定CacheMode來改變WebView的快取機制;而AppCache的快取策略是由manifest檔案控制的,也就是說是由web頁面開發者控制的。

最後說一下,其實很多時候,這兩類快取是共同在工作的,當manifest檔案沒有控制某些資源載入時,例如我上面寫的xxx.appcache檔案裡,NETWORK section下面用的是*號,意思是所有不快取的檔案都要去網路載入。此時,這些資源就會走到WebView自帶的快取機制去,結合WebView的CacheMode,我們實際上對這些檔案進行了一次WebView自帶的快取。搞清楚這兩類快取的原理有利於我們更好的設計自己的頁面和App,儘可能減少網路請求,提高App執行效率。

相關文章