本文首发于微信公众号「玉刚说」
原文链接:Android Webview H5 秒开方案实现
前言
现在许多app都嵌入了H5页面, 然而WebView加载速度慢这个问题却一直影响着用户的体验, 所以本文就如何提高H5页面的加载速度展开讨论。
问题原因
首先我们需要知道为什么WebView的加载速度那么慢。H5页面的渲染速度其实主要取决于两个
- js解析效率
如果js文件较多、解析比较复杂, 就会导致渲染速度较慢。或者手机的硬件性能比较差的话, 也会导致渲染速度比较慢。 - 页面资源的下载
一般加载一个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 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-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中缓存模式的解释:
- 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性能、体验分析与优化