Android Webview H5 秒开方案实现

45,156 阅读14分钟

本文首发于微信公众号「玉刚说」

原文链接:Android Webview H5 秒开方案实现

前言

现在许多app都嵌入了H5页面, 然而WebView加载速度慢这个问题却一直影响着用户的体验, 所以本文就如何提高H5页面的加载速度展开讨论。

问题原因

首先我们需要知道为什么WebView的加载速度那么慢。H5页面的渲染速度其实主要取决于两个

  1. js解析效率
    如果js文件较多、解析比较复杂, 就会导致渲染速度较慢。或者手机的硬件性能比较差的话, 也会导致渲染速度比较慢。
  2. 页面资源的下载
    一般加载一个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-ModifiedETagExpiresCache-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性能、体验分析与优化

欢迎关注我的微信公众号「玉刚说」,接收第一手技术干货