Android Webview H5 秒开方案实现

作者: 迷途小码农h | 来源:发表于2019-04-04 14:34 被阅读8次

    前言

    现在许多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的加载速度其实涉及到的方面很多, 需要注意的细节也很多, 没有办法一概而论。大家需要按照公司的业务需要量体裁衣, 按需配置。

    相关文章

      网友评论

        本文标题:Android Webview H5 秒开方案实现

        本文链接:https://www.haomeiwen.com/subject/hknfiqtx.html