本文首发于微信公众号「玉刚说」css
原文连接:Android Webview H5 秒开方案实现html
如今许多app都嵌入了H5页面, 然而WebView加载速度慢这个问题却一直影响着用户的体验, 因此本文就如何提升H5页面的加载速度展开讨论。前端
首先咱们须要知道为何WebView的加载速度那么慢。H5页面的渲染速度其实主要取决于两个git
对于上面的第一点, 其实主要是由前端代码和手机硬件决定的, 由于咱们这里讨论的是对于app的性能优化, 暂时不考虑, 因此咱们能够从第二点作文章, 主要思路就是一些资源文件都使用App本地资源, 而不须要从网络下载, 从而提升页面的打开速度。github
以加载玉刚说的renyugang.io/post/75这个页面为例。web
首先将一些资源文件放在本地的assets目录, 而后重写WebViewClient的shouldInterceptRequest(WebView view, String url)和shouldInterceptRequest(WebView view, WebResourceRequest request)这两个方法, 对访问地址进行拦截, 当url地址命中本地配置的url时, 使用本地资源替代, 不然就使用网络上的资源。segmentfault
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以上的系统使用。我我的的建议是把这两个方法都重写了。
咱们再看一个有意思的现象, 在不配置本地资源的时候, 咱们第一次打开页面, 产生了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中缓存模式的解释:
因此咱们通常设置为默认的缓存模式就能够了。关于缓存的配置, 主要仍是靠web前端和后台设置。
除了WebView自带的缓存, 还有Application Cache缓存, Dom Storage缓存, Web SQL Database缓存, IndexedDB缓存。可是剩下的几种缓存, 根据官方文档, AppCache已经不推荐使用了, 标准也不会再支持。而其余的几种也不是文件缓存, 和咱们今天讨论的主题不符, 因此我也再也不介绍了。有兴趣能够看H5 缓存机制浅析 移动端 Web 加载性能优化和Android:手把手教你构建 全面的WebView 缓存机制 & 资源加载方案
本地Webview初始化都要很多时间, 首次初始化webview与第二次初始化不一样,首次会比第二次慢不少。缘由预计是webview首次初始化后,即便 webview 已经释放,但一些webview 共用的全局服务或资源对象仍没有释放,第二次初始化时不须要再生成这些对象从而变快。咱们能够在Application预先初始化好WebView, 当第二次初始化WebView的时候速度就快多了, 或者直接将其拿来使用。
预加载数据就是在客户端初始化WebView的同时,直接由native开始网络请求数据, 当页面初始化完成后,向native获取其代理请求的数据, 数据请求和WebView初始化能够并行进行,缩短整体的页面加载时间。简单来讲就是配置一个预加载列表,在APP启动或某些时机时提早去请求,这个预加载列表须要包含所需H5模块的页面和资源, 客户端能够接管全部请求的缓存,不走webview默认缓存逻辑, 自行实现缓存机制, 原理其实就是拦截WebViewClient的那两个shouldInterceptRequest方法。
离线包的意思就是将H5的页面和资源进行打包后下发到客户端,并由客户端直接解压到本地储存中。优势是因为其本地化,首屏加载速度快,用户体验更为接近原生, 能够不依赖网络,离线运行, 缺点就是开发流程/更新机制复杂化, 须要客户端、甚至服务端的共同协做。这里我以Hybrid App技术解析 -- 实战篇中提到的思路为例子供你们参考。
资源:
流程:
关于离线包的机制须要注意的问题还不少, 本文确定没法照顾彻底, 你们能够参考移动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性能、体验分析与优化