本文首發於微信公衆號「玉剛說」css
原文連接:Android Webview H5 秒開方案實現html
如今許多app都嵌入了H5頁面, 然而WebView加載速度慢這個問題卻一直影響着用戶的體驗, 因此本文就如何提升H5頁面的加載速度展開討論。前端
首先咱們須要知道爲何WebView的加載速度那麼慢。H5頁面的渲染速度其實主要取決於兩個git
對於上面的第一點, 其實主要是由前端代碼和手機硬件決定的, 由於咱們這裏討論的是對於app的性能優化, 暫時不考慮, 因此咱們能夠從第二點作文章, 主要思路就是一些資源文件都使用App本地資源, 而不須要從網絡下載, 從而提升頁面的打開速度。github
以加載玉剛說的renyugang.io/post/75這個頁面爲例。web
首先將一些資源文件放在本地的assets目錄, 而後重寫WebViewClient的shouldInterceptRequest(WebView view, String url)和shouldInterceptRequest(WebView view, WebResourceRequest request)這兩個方法, 對訪問地址進行攔截, 當url地址命中本地配置的url時, 使用本地資源替代, 不然就使用網絡上的資源。segmentfault
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以上的系統使用。我我的的建議是把這兩個方法都重寫了。
咱們再看一個有意思的現象, 在不配置本地資源的時候, 咱們第一次打開頁面, 產生了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中緩存模式的解釋:
因此咱們通常設置爲默認的緩存模式就能夠了。關於緩存的配置, 主要仍是靠web前端和後臺設置。
除了WebView自帶的緩存, 還有Application Cache緩存, Dom Storage緩存, Web SQL Database緩存, IndexedDB緩存。可是剩下的幾種緩存, 根據官方文檔, AppCache已經不推薦使用了, 標準也不會再支持。而其餘的幾種也不是文件緩存, 和咱們今天討論的主題不符, 因此我也再也不介紹了。有興趣能夠看H5 緩存機制淺析 移動端 Web 加載性能優化和Android:手把手教你構建 全面的WebView 緩存機制 & 資源加載方案
本地Webview初始化都要很多時間, 首次初始化webview與第二次初始化不一樣,首次會比第二次慢不少。緣由預計是webview首次初始化後,即便 webview 已經釋放,但一些webview 共用的全局服務或資源對象仍沒有釋放,第二次初始化時不須要再生成這些對象從而變快。咱們能夠在Application預先初始化好WebView, 當第二次初始化WebView的時候速度就快多了, 或者直接將其拿來使用。
預加載數據就是在客戶端初始化WebView的同時,直接由native開始網絡請求數據, 當頁面初始化完成後,向native獲取其代理請求的數據, 數據請求和WebView初始化能夠並行進行,縮短整體的頁面加載時間。簡單來講就是配置一個預加載列表,在APP啓動或某些時機時提早去請求,這個預加載列表須要包含所需H5模塊的頁面和資源, 客戶端能夠接管全部請求的緩存,不走webview默認緩存邏輯, 自行實現緩存機制, 原理其實就是攔截WebViewClient的那兩個shouldInterceptRequest方法。
離線包的意思就是將H5的頁面和資源進行打包後下發到客戶端,並由客戶端直接解壓到本地儲存中。優勢是因爲其本地化,首屏加載速度快,用戶體驗更爲接近原生, 能夠不依賴網絡,離線運行, 缺點就是開發流程/更新機制複雜化, 須要客戶端、甚至服務端的共同協做。這裏我以Hybrid App技術解析 -- 實戰篇中提到的思路爲例子供你們參考。
資源:
流程:
關於離線包的機制須要注意的問題還不少, 本文確定沒法照顧徹底, 你們能夠參考移動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性能、體驗分析與優化