一、为什么需要对webview进行缓存

  1. 每次使用 H5页面时,用户都需要重新加载 Android WebView的H5 页面
  2. 每加载一个 H5页面,都会产生较多网络请求,HTML 主 URL 自身的请求,HTML外部引用的JS、CSS、字体文件,图片也是一个独立的 HTTP 请求,每一个请求都串行的,这么多请求串起来,这导致 H5页面资源加载缓慢。

上述问题导致了Android WebView的H5 页面体验 与 原生Native 存在较大差距。

二、解决方案

  • 前端H5的缓存机制(WebView 自带)
  • 资源预加载
  • 资源拦截

三、缓存机制

Android WebView自带的缓存机制有5种:

1、浏览器 缓存机制

根据 HTTP 协议头里的 Cache-Control(或 Expires)和 Last-Modified(或 Etag)等字段来控制文件缓存的机制。

例如Last-Modified:标识文件在服务器上的最新更新时间

下次请求时,如果文件缓存过期,浏览器通过 If-Modified-Since 字段带上这个时间,发送给服务器,由服务器比较时间戳来判断文件是否有修改。如果没有修改,服务器返回304告诉浏览器继续使用缓存;如果有修改,则返回200,同时返回最新的文件。

2、 Application Cache 缓存机制

以文件为单位进行缓存,且文件有一定更新机制(类似于浏览器缓存机制)AppCache 原理有两个关键点:manifest 属性和 manifest 文件。

<!DOCTYPE html>
<html manifest="demo_html.appcache">
// HTML 在头中通过 manifest 属性引用 manifest 文件
// manifest 文件:就是上面以 appcache 结尾的文件,是一个普通文件文件,列出了需要缓存的文件
// 浏览器在首次加载 HTML 文件时,会解析 manifest 属性,并读取 manifest 文件,获取 Section:CACHE MANIFEST 下要缓存的文件列表,再对文件缓存
<body>
...
</body>
</html>
 
// 原理说明如下:
// AppCache 在首次加载生成后,也有更新机制。被缓存的文件如果要更新,需要更新 manifest 文件
// 因为浏览器在下次加载时,除了会默认使用缓存外,还会在后台检查 manifest 文件有没有修改(byte by byte)发现有修改,就会重新获取 manifest 文件,对 Section:CACHE MANIFEST 下文件列表检查更新
// manifest 文件与缓存文件的检查更新也遵守浏览器缓存机制
// 如用户手动清了 AppCache 缓存,下次加载时,浏览器会重新生成缓存,也可算是一种缓存的更新
// AppCache 的缓存文件,与浏览器的缓存文件分开存储的,因为 AppCache 在本地有 5MB(分 HOST)的空间限制

实现:

// 通过设置WebView的settings来实现
 WebSettings settings = getSettings();

 String cacheDirPath = context.getFilesDir().getAbsolutePath()+"cache/";
 settings.setAppCachePath(cacheDirPath);
 // 1. 设置缓存路径

 settings.setAppCacheMaxSize(20*1024*1024);
 // 2. 设置缓存大小

 settings.setAppCacheEnabled(true);
 // 3. 开启Application Cache存储机制

3、Dom Storage 缓存机制

通过存储字符串的 Key - Value 对来提供

DOM Storage 分为 sessionStorage & localStorage; 二者使用方法基本相同,区别在于作用范围不同:
a. sessionStorage:具备临时性,即存储与页面相关的数据,它在页面关闭后无法使用
b.localStorage:具备持久性,即保存的数据在页面关闭后也可以使用。

// 通过设置 `WebView`的`Settings`类实现
 WebSettings settings = getSettings();
 // 开启DOM storage
 settings.setDomStorageEnabled(true);

4、 Web SQL Database 缓存机制

基于 SQL 的数据库存储机制

// 通过设置WebView的settings实现
 WebSettings settings = getSettings();
 // 设置缓存路径
 String cacheDirPath = context.getFilesDir().getAbsolutePath()+"cache/";
 settings.setDatabasePath(cacheDirPath);
 // 开启 数据库存储机制
 settings.setDatabaseEnabled(true);

5、Indexed Database 缓存机制
属于 NoSQL 数据库,通过存储字符串的 Key - Value 对来提供

// 通过设置WebView的settings实现
WebSettings settings = getSettings();
// 只需设置支持JS就自动打开IndexedDB存储机制
// Android 在4.4开始加入对 IndexedDB 的支持,只需打开允许 JS 执行的开关就好了。
settings.setJavaScriptEnabled(true);

6、File System 缓存机制(H5页面新加入的缓存机制,虽然Android WebView暂时不支持,但会进行简单介绍)

为 H5页面的数据 提供一个虚拟的文件系统,可进行文件(夹)的创建、读、写、删除、遍历等操作,就像 Native App 访问本地文件系统一样。

四、资源预加载

在Android 的BaseApplication里初始化一个WebView对象(用于加载常用的H5页面资源);当需使用这些页面时再从BaseApplication里取过来直接使用。

五、自身构建缓存

为了有效解决 Android WebView 的性能问题,除了使用 Android WebView 自身的缓存机制,还可以自己针对某一需求场景构建缓存机制。

实现步骤

  1. 事先将更新频率较低、常用 & 固定的H5静态资源 文件(如JS、CSS文件、图片等) 放到本地
  2. 拦截H5页面的资源网络请求 并进行检测
  3. 如果检测到本地具有相同的静态资源 就 直接从本地读取进行替换 而 不发送该资源的网络请求 到 服务器获取
// 假设现在需要拦截一个图片的资源并用本地资源进行替代
 
mWebview.setWebViewClient(new WebViewClient() {
     // 重写 WebViewClient  的  shouldInterceptRequest ()
     // API 21 以下用shouldInterceptRequest(WebView view, String url)
     // API 21 以上用shouldInterceptRequest(WebView view, WebResourceRequest request)
     // 下面会详细说明

      // API 21 以下用shouldInterceptRequest(WebView view, String url)
     @Override
     public WebResourceResponse shouldInterceptRequest(WebView view, String url) {

         // 步骤1:判断拦截资源的条件,即判断url里的图片资源的文件名
         if (url.contains("logo.gif")) {
         // 假设网页里该图片资源的地址为:http://abc.com/imgage/logo.gif
         // 图片的资源文件名为:logo.gif

             InputStream is = null;
             // 步骤2:创建一个输入流

             try {
                 is =getApplicationContext().getAssets().open("images/abc.png");
                 // 步骤3:获得需要替换的资源(存放在assets文件夹里)
                 // a. 先在app/src/main下创建一个assets文件夹
                 // b. 在assets文件夹里再创建一个images文件夹
                 // c. 在images文件夹放上需要替换的资源(此处替换的是abc.png图片)

             } catch (IOException e) {
                 e.printStackTrace();
             }

             // 步骤4:替换资源
             WebResourceResponse response = new WebResourceResponse("image/png",
                     "utf-8", is);
             // 参数1:http请求里该图片的Content-Type,此处图片为image/png
             // 参数2:编码类型
             // 参数3:存放着替换资源的输入流(上面创建的那个)
             return response;
         }

         return super.shouldInterceptRequest(view, url);
     }

     
    // API 21 以上用shouldInterceptRequest(WebView view, WebResourceRequest request)
     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
     @Override
     public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {

        // 步骤1:判断拦截资源的条件,即判断url里的图片资源的文件名
         if (request.getUrl().toString().contains("logo.gif")) {
         // 假设网页里该图片资源的地址为:http://abc.com/imgage/logo.gif
         // 图片的资源文件名为:logo.gif

             InputStream is = null;
             // 步骤2:创建一个输入流

             try {
                 is = getApplicationContext().getAssets().open("images/abc.png");
                  // 步骤3:获得需要替换的资源(存放在assets文件夹里)
                 // a. 先在app/src/main下创建一个assets文件夹
                 // b. 在assets文件夹里再创建一个images文件夹
                 // c. 在images文件夹放上需要替换的资源(此处替换的是abc.png图片

             } catch (IOException e) {
                 e.printStackTrace();
             }

             // 步骤4:替换资源
             WebResourceResponse response = new WebResourceResponse("image/png",
                     "utf-8", is);
             // 参数1:http请求里该图片的Content-Type,此处图片为image/png
             // 参数2:编码类型
             // 参数3:存放着替换资源的输入流(上面创建的那个)
             return response;
         }
         return super.shouldInterceptRequest(view, request);
     }

});

}