本文首發於微信公眾號「玉剛說」
前言
現在許多app都嵌入了H5頁面, 然而WebView載入速度慢這個問題卻一直影響著使用者的體驗, 所以本文就如何提高H5頁面的載入速度展開討論。
問題原因
首先我們需要知道為什麼WebView的載入速度那麼慢。H5頁面的渲染速度其實主要取決於兩個
- js解析效率
如果js檔案較多、解析比較複雜, 就會導致渲染速度較慢。或者手機的硬體效能比較差的話, 也會導致渲染速度比較慢。 - 頁面資源的下載
一般載入一個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個實際發出的網路請求:
優化後, 只有一個實際發出的網路請求。並且為了和網路的資源圖片做區分, 我在兩張本地圖片中加了“本地”的水印, 能明顯看到這時候載入的是本地圖片:
另外再提一點, 對於WebViewClient的shouldInterceptRequest(WebView view, String url)和shouldInterceptRequest(WebView view, WebResourceRequest request)這兩個方法, 經本人親測, 重寫其中的任何一個都能生效, 後面一個shouldInterceptRequest(WebView view, WebResourceRequest request)一般是5.0以上的系統使用。我個人的建議是把這兩個方法都重寫了。
關於WebView的快取
我們再看一個有意思的現象, 在不配置本地資源的時候, 我們第一次開啟頁面, 產生了n多個請求。但是當我們退出後再次開啟這個頁面(沒有設定載入本地資源)的時候, 居然只發生了一次請求, 這現象與載入本地資源十分相似。
這是為什麼呢?
我們解除安裝app, 抓包, 再次開啟頁面, 以banner圖片請求的舉例。
我們觀察這個請求的response的headers中的引數, 注意到這麼幾個欄位:Last-Modified
、ETag
、Expires
、Cache-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分鐘, 我們再次開啟頁面, 觀察請求。
在上面這個請求中, 我們在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效能、體驗分析與優化