Android 拦截WebView加载URL,控制其加载CSS、JS资源

绪论

最近在项目中有了这样一个需求,我们都知道WebView加载网页可以缓存,但是web端想让客服端根据需求来缓存网页,也就是说web端在设置了http响应头,我根据这个头来拦截WebView加载网页,去执行网络加载还是本地缓存加载。这个需求之前一直没听说过,在网上搜了一下,发现有拦截WebView加载网页这个方法,研究了一下,最终实现了,今天小编分享给大家这个开发经验:

WebView缓存机制

1.缓存模式

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,都使用缓存中的数据

2.缓存路径

  • /data/data/包名/cache/
  • /data/data/包名/database/webview.db
  • /data/data/包名/database/webviewCache.db

3.设置缓存模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mWebSetting.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);
}

4.清除缓存

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
/**
* 清除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了,不过这样读写比较麻烦,也比较费时,但是却给后台减少了不小的压力。看一下代码具体怎么实现的吧。

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
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~~