以公司的新闻资讯类产品为例,总结一下加载提速方案。
首先对于新闻资讯页面来说,不同的页面之间有很多重复的资源,这些资源可以直接保存在本地复用:将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) { String fileName = getFileNameByUrl(request.getUrl().toString()); if (!fileInCache(fileName)) { return super.shouldInterceptRequest(view, request); } else { 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); 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() { }
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缓存读取时加锁保证线程安全,这里的锁可以选择读写锁。