本文共 10549 字,大约阅读时间需要 35 分钟。
最近在项目中有了这样一个需求,我们都知道WebView加载网页可以缓存,但是web端想让客服端根据需求来缓存网页,也就是说web端在设置了http响应头,我根据这个头来拦截WebView加载网页,去执行网络加载还是本地缓存加载。这个需求之前一直没听说过,在网上搜了一下,发现有拦截WebView加载网页这个方法,研究了一下,最终实现了,今天小编分享给大家这个开发经验:
Android的WebView有五种缓存模式
1.LOAD_CACHE_ONLY //不使用网络,只读取本地缓存数据 2.LOAD_DEFAULT //根据cache-control决定是否从网络上取数据。 3.LOAD_CACHE_NORMAL //API level 17中已经废弃, 从API level 11开始作用同LOAD_DEFAULT模式 4.LOAD_NO_CACHE //不使用缓存,只从网络获取数据 5.LOAD_CACHE_ELSE_NETWORK //只要本地有,无论是否过期,或者no-cache,都使用缓存中的数据/data/data/包名/cache/
/data/data/包名/database/webview.db /data/data/包名/database/webviewCache.dbmWebSetting.setLoadWithOverviewMode(true); mWebSetting.setDomStorageEnabled(true); mWebSetting.setAppCacheMaxSize(1024 * 1024 * 8);//设置缓存大小 //设置缓存路径 appCacheDir = Environment.getExternalStorageDirectory().getPath() + "/xxx/cache"; File fileSD = new File(appCacheDir); if (!fileSD.exists()) { fileSD.mkdir(); } mWebSetting.setAppCachePath(appCacheDir); mWebSetting.setAllowContentAccess(true); mWebSetting.setAppCacheEnabled(true); if (CheckHasNet.isNetWorkOk(context)) { //有网络网络加载 mWebSetting.setCacheMode(WebSettings.LOAD_DEFAULT); } else { //无网时本地缓存加载 mWebSetting.setCacheMode(WebSettings.LOAD_CACHE_ONLY); }
/** * 清除WebView缓存 */ public void clearWebViewCache(){ //清理Webview缓存数据库 try { deleteDatabase("webview.db"); deleteDatabase("webviewCache.db"); } catch (Exception e) { e.printStackTrace(); } //WebView 缓存文件 File appCacheDir = new File(getFilesDir().getAbsolutePath()+APP_CACAHE_DIRNAME); Log.e(TAG, "appCacheDir path="+appCacheDir.getAbsolutePath()); File webviewCacheDir = new File(getCacheDir().getAbsolutePath()+"/webviewCache"); Log.e(TAG, "webviewCacheDir path="+webviewCacheDir.getAbsolutePath()); //删除webview 缓存目录 if(webviewCacheDir.exists()){ deleteFile(webviewCacheDir); } //删除webview 缓存 缓存目录 if(appCacheDir.exists()){ deleteFile(appCacheDir); } }
好了,我们了解了WebView的缓存缓存机制了之后来看看到底怎么拦截WebView加载网页:
1.要想拦截WebView加载网页我们必须重写WebViewClient类,在WebViewClient类中我们重写shouldInterceptRequest()方法,看方法名一目了然,拦截http请求,肯定是这个方法。
2.获取http请求的头,看是否包含所设置的flag,如果包含这个flag说明web端想让我们保存这个html,那么我们改怎么手动保存这个html呢? 1)获取url的connection 2)利用connection.getHeaderField(“flag”)获取http请求头信息 3)得到请求的内容区数据的类型String contentType = connection.getContentType(); 4)获取html的编码格式 5)将html的内容写入文件(具体代码下面会介绍) *3.注意:因为控制WebView加载网页的方法需要三个参数 public WebResourceResponse(String mimeType, String encoding, InputStream data) mimeType:也就是我们第3步获取的内容区数据的类型 encoding:就是html的编码格式 data:本地写入的html文件*那么问题来了,我们可以把html代码写到本地缓存文件中,而这个html所对应的mimeType和encoding我们存到哪里呢?因为http的头信息是http请求的属性,我们存到SP中?存到数据库中?好像都不行,无法对应关系啊。这块小编想了好久,因为小编没怎么写过文件读取这一块,最后想到把这两个参数一起存到html文件开始的几个字节,每次加载先读取这两个参数就OK了,不过这样读写比较麻烦,也比较费时,但是却给后台减少了不小的压力。看一下代码具体怎么实现的吧。
class MyWebClient extends WebViewClient { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { if (!"get".equals(request.getMethod().toLowerCase())) { return super.shouldInterceptRequest(view, request); } String url = request.getUrl().toString(); //todo:计算url的hash String md5URL = YUtils.md5(url); //读取缓存的html页面 File file = new File(appCacheDir + File.separator + md5URL); if (file.exists()) { FileInputStream fileInputStream = null; try { fileInputStream = new FileInputStream(file); Log.e(">>>>>>>>>", "读缓存"); return new WebResourceResponse(YUtils.readBlock(fileInputStream), YUtils.readBlock(fileInputStream), fileInputStream); } catch (FileNotFoundException e) { e.printStackTrace(); } return null; } try { URL uri = new URL(url); URLConnection connection = uri.openConnection(); InputStream uristream = connection.getInputStream(); String cache = connection.getHeaderField("Ddbuild-Cache"); String contentType = connection.getContentType(); //text/html; charset=utf-8 String mimeType = ""; String encoding = ""; if (contentType != null && !"".equals(contentType)) { if (contentType.indexOf(";") != -1) { String[] args = contentType.split(";"); mimeType = args[0]; String[] args2 = args[1].trim().split("="); if (args.length == 2 && args2[0].trim().toLowerCase().equals("charset")) { encoding = args2[1].trim(); } else { encoding = "utf-8"; } } else { mimeType = contentType; encoding = "utf-8"; } } if ("1".equals(cache)) { //todo:缓存uristream FileOutputStream output = new FileOutputStream(file); int read_len; byte[] buffer = new byte[1024]; YUtils.writeBlock(output, mimeType); YUtils.writeBlock(output, encoding); while ((read_len = uristream.read(buffer)) > 0) { output.write(buffer, 0, read_len); } output.close(); uristream.close(); FileInputStream fileInputStream = new FileInputStream(file); YUtils.readBlock(fileInputStream); YUtils.readBlock(fileInputStream); Log.e(">>>>>>>>>", "读缓存"); return new WebResourceResponse(mimeType, encoding, fileInputStream); } else { Log.e(">>>>>>>>>", "网络加载"); return new WebResourceResponse(mimeType, encoding, uristream); } } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } } //这里面读写操作比较多,还有截取那两个属性的字符串稍微有点麻烦 /** * int转byte * by黄海杰 at:2015-10-29 16:15:06 * @param iSource * @param iArrayLen * @return */ public static byte[] toByteArray(int iSource, int iArrayLen) { byte[] bLocalArr = new byte[iArrayLen]; for (int i = 0; (i < 4) && (i < iArrayLen); i++) { bLocalArr[i] = (byte) (iSource >> 8 * i & 0xFF); } return bLocalArr; } /** * byte转int * by黄海杰 at:2015-10-29 16:14:37 * @param bRefArr * @return */ // 将byte数组bRefArr转为一个整数,字节数组的低位是整型的低字节位 public static int toInt(byte[] bRefArr) { int iOutcome = 0; byte bLoop; for (int i = 0; i < bRefArr.length; i++) { bLoop = bRefArr[i]; iOutcome += (bLoop & 0xFF) << (8 * i); } return iOutcome; } /** * 写入JS相关文件 * by黄海杰 at:2015-10-29 16:14:01 * @param output * @param str */ public static void writeBlock(OutputStream output, String str) { try { byte[] buffer = str.getBytes("utf-8"); int len = buffer.length; byte[] len_buffer = toByteArray(len, 4); output.write(len_buffer); output.write(buffer); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } /** * 读取JS相关文件 * by黄海杰 at:2015-10-29 16:14:19 * @param input * @return */ public static String readBlock(InputStream input) { try { byte[] len_buffer = new byte[4]; input.read(len_buffer); int len = toInt(len_buffer); ByteArrayOutputStream output = new ByteArrayOutputStream(); int read_len = 0; byte[] buffer = new byte[len]; while ((read_len = input.read(buffer)) > 0) { len -= read_len; output.write(buffer, 0, read_len); if (len <= 0) { break; } } buffer = output.toByteArray(); output.close(); return new String(buffer,"utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } //为了加密我们的html我们把url转成md5 /** * 字符串转MD5 * by黄海杰 at:2015-10-29 16:15:32 * @param string * @return */ public static String md5(String string) { byte[] hash; try { hash = MessageDigest.getInstance("MD5").digest(string.getBytes("UTF-8")); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Huh, MD5 should be supported?", e); } catch (UnsupportedEncodingException e) { throw new RuntimeException("Huh, UTF-8 should be supported?", e); } StringBuilder hex = new StringBuilder(hash.length * 2); for (byte b : hash) { if ((b & 0xFF) < 0x10) hex.append("0"); hex.append(Integer.toHexString(b & 0xFF)); } return hex.toString(); }
功能虽然实现了,但是发现一个比较棘手的问题,就是shouldInterceptRequest()方法有两个:
1.public WebResourceResponse shouldInterceptRequest(WebView view, String url) { return super.shouldInterceptRequest(view, url); } 2.public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {} 重载的方法,第一个是已经废弃了的,SDK 20以下的会执行1,SDK20以上的会执行2,那么问题又来了,因为我们在获取http请求的时候要判断是post()请求还是get()请求,如果是post请求我们就网络加载,而get请求才去加载本地缓存,因为post请求需要参数。所以大家可以看到我上面仅仅实现了SDK20以上的新方法,而没有去关SDK20以下废弃的那个函数,因为废弃的那个函数根本获取不到请求方式,不知道是不是因为这个原因才将这个方法废弃的。这一块小编会继续研究的,一定要解决这个问题,小编已经有了思路不知道能不能实现,接下来小编会去研究一下2014年新出的CrossWalk这个浏览器插件,据说重写了底层,比webview能更好的兼容h5新特性,更稳定,屏蔽安卓不同版本的webview的兼容性问题 生命就在于折腾,小编就喜欢折腾,将Android折腾到底O(∩_∩)O~~