Android Webview H5 秒开方案实现

2019-04-18 H5案例分享 H5案例分享

前言

 

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

 

问题原因

 

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

 

 

  1. js解析效率  
    如果js文件较多、解析比较复杂, 就会导致渲染速度较慢。或者手机的硬件性能比较差的话, 也会导致渲染速度比较慢。
  2. 页面资源的下载  
    一般加载一个H5页面, 都会产生较多的网络请求, 如图片、js文件、css文件等, 需要将这些资源都下载完成之后才能完成渲染, 这样也会导致页面渲染速度变慢

 

对于上面的第一点, 其实主要是由前端代码和手机硬件决定的, 因为我们这里讨论的是对于app的性能优化, 暂时不考虑, 所以我们可以从第二点做文章, 主要思路就是一些资源文件都使用App本地资源, 而不需要从网络下载, 从而提高页面的打开速度。

 

代码实现

 

以加载玉刚说的http://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

 

这个库的介绍链接在这里https://my.oschina.net/yale8848/blog/1544298, 据作者说主要是为了解决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
https://github.com/mundane799699/AcceleratedWebView

 

参考:

Android:手把手教你构建 全面的WebView 缓存机制 & 资源加载方案
WebView缓存原理分析和应用
H5 和移动端 WebView 缓存机制解析与实战
腾讯祭出大招VasSonic,让你的H5页面首屏秒开!
《移动端本地 H5 秒开方案探索与实现》
移动 H5 首屏秒开优化方案探讨
美团大众点评 Hybrid 化建设
H5 缓存机制浅析 移动端 Web 加载性能优化
QQ会员基于 Hybrid 的高质量 H5 架构实践
从WebView缓存聊到Http 的缓存机制 | 掘金技术征文
美团: WebView性能、体验分析与优化

 

 

— — — END — — —

 

 

 

本文由玉刚说写作平台提供写作赞助
原作者:mundane
赞助金额:200元

 

 

(转自微信公众号「玉刚说」的Android Webview H5 秒开方案实现,链接:https://mp.weixin.qq.com/s/XfBt_gTw0gN7tXzuyP4PTw

 

 

以上为“H5案例分享”团队转载文章,版权归原作者所有。


以上为“H5案例分享”团队原创文章,转载请注明出处!

标签

分享:
海报
案列链接
下载截图
收藏案例
已收藏
分享到微信朋友圈
打开微信,点击底部的“发现”,
使用“扫一扫”即可将网页分享至朋友圈。

相关推荐:

中国移动:中国移动

JS表单验证-12个常用的JS表单验证

关注微信号:h5-share,获取更多创意H5案例分享!也可访问H5案例分享网站www.h5anli.com,搜索查阅~

阅读原文

微信扫一扫
关注该公众号

收藏

微信极速登录/注册

对汉语支持不错,别对小五说英文

微信公众号

提交案例

请pc端输入网址

"h5anli.com"

下载截图压缩包~

标签已提交成功

小编正在审核

请等待一下下啦

哎呀

标签已存在

换个新的呗~