以公司的新闻资讯类产品为例,总结一下加载提速方案。
首先对于新闻资讯页面来说,不同的页面之间有很多重复的资源,这些资源可以直接保存在本地复用:将css、js和图片文件放到Assets中,应用可以拦截这部分的请求转而加载这些文件。
其次,WebView的新建与初始化也是比较耗时的,因此可以使用WebView缓存池进行复用。
再次是预加载,在浏览信息流时,详情页url的主要html内容已经被下载到了内存中,可以直接通过WebView.loadDataWithBaseUrl()加载。接下来介绍这3种方案的具体实现。

资源缓存

WebView 请求任何资源时都会回调shouldInterceptRequest()方法,此时可以将 WebView 需要请求的资源替换为本地资源以提升加载速度。
本地资源可以保存在Assets或文件系统中,如果保存在Assets中,文件的安全性会得到保证,没有出错或者被修改的风险,但是无法实时更新,只能依赖客户端发版更新文件;如果保存在文件系统中,可以做到实时更新,但是下载文件时可能出错,使用时需要对文件进行校验。

这里以保存在Assets中为例,当WebView回调shouldInterceptRequest()时,如果发现当前的文件可以使用Assets中的缓存,即可将其包装成WebResourceResponse,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
mWebView.setWebViewClient(new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
// 根据url得到文件名
String fileName = getFileNameByUrl(request.getUrl().toString());
if (!fileInCache(fileName)) {
// 如果当前文件不在缓存列表中, 不使用缓存
return super.shouldInterceptRequest(view, request);
} else {
// 当前文件可以使用缓存, 根据后缀判断 mimetype
InputStream inputStream = null;
String mimeType = null;
if (fileName.endsWith("css")) {
mimeType = "text/css";
} else if (fileName.endsWith("js")) {
mimeType = "text/javascript";
} else if (fileName.endsWith("png")) {
mimeType = "image/png";
}
if (mimeType == null) {
return null;
}
try {
inputStream = App.getContext().getAssets().open(fileName);
} catch (IOException e) {
Log.e(LOG_TAG, "read file IOException: " + e.getMessage());
}
if (inputStream != null) {
WebResourceResponse response = new WebResourceResponse(
mimeType, "utf-8", inputStream);
// 解决css、js的跨域问题
Map<String, String> headers = new HashMap<>();
headers.put("Access-Control-Allow-Origin", "*");
headers.put("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
headers.put("Access-Control-Max-Age", "3600");
headers.put("Access-Control-Allow-Headers", "Content-Type,Access-Token,Authorization");
response.setResponseHeaders(headers);
return response;
}
return null;
}
}
});

WebView缓存池

平时使用WebView的时候我们都是动态新建并添加到ViewGroup中,新建时就传入了Context,那如果使用缓存池,Context该怎么传呢?

一种方案是直接用ApplicationContext新建WebView;另一种方案是使用MutableContextWrapper,如果在某个Activity中被使用了就改为该Activity的Context,回收时改为ApplicationContext。一个简易版的WebView缓存池实现如下。

注意在APP启动时应该就要调用WebViewPool的初始化方法新建一个WebView,不然启动第一个包含WebView的页面时耗时也会很久。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class WebViewPool {

private List<DetailWebView> mIdleWebViewList;
private List<DetailWebView> mUsingWebViewList;

private static class Holder {
private static WebViewPool sInstance = new WebViewPool();
}

public static WebViewPool getInstance() {
return Holder.sInstance;
}

private WebViewPool() {
}

/**
* 在 APP 启动时调用, 直接新建一个备用的WebView
*/
public void init() {
mIdleWebViewList = new CopyOnWriteArrayList<>();
mUsingWebViewList = new ArrayList<>();
MutableContextWrapper contextWrapper = new MutableContextWrapper(App.getContext());
DetailWebView detailWebView = new DetailWebView(contextWrapper);
mIdleWebViewList.add(detailWebView);
}

public DetailWebView acquireWebView(Context context) {
if (mIdleWebViewList != null && mIdleWebViewList.size() > 0) {
DetailWebView webView = mIdleWebViewList.remove(0);
MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext();
contextWrapper.setBaseContext(context);
mUsingWebViewList.add(webView);
return webView;
} else {
MutableContextWrapper contextWrapper = new MutableContextWrapper(context);
DetailWebView webView = new DetailWebView(contextWrapper);
mUsingWebViewList.add(webView);
return webView;
}
}

public void recycleWebView(DetailWebView webView) {
if (webView == null) {
return;
}
ViewGroup viewParent = (ViewGroup) webView.getParent();
if (viewParent != null) {
viewParent.removeView(webView);
}
webView.loadUrl("about:blank");
if (mUsingWebViewList != null && mUsingWebViewList.contains(webView)) {
mUsingWebViewList.remove(webView);
MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext();
contextWrapper.setBaseContext(App.getContext());
webView.setWebViewClient(null);
webView.setWebChromeClient(null);
mIdleWebViewList.add(webView);
} else {
webView.clearHistory();
webView.destroy();
}
}
}

预加载

预加载主体的实现比较简单,在信息流的RecyclerView进入IDLE状态时,判断当前有多少个条目曝光,然后启动对应数量的子线程开始下载数据为String,用户进入某个条目时,如果数据下载完毕就可以直接通过WebView.loadDataWithBaseURL(mUrl, data, "text/html", "utf-8", null)展示数据。
应用对预加载下载的数据是需要统一管理的,可以考虑使用LRU缓存。不过由于是多线程下载数据,需要对在对LRU缓存读取时加锁保证线程安全,这里的锁可以选择读写锁。