shouldInterceptRequest 是WebViewClient的一个方法,官方的说明:
/**
* Notify the host application of a resource request and allow the
* application to return the data. If the return value is null, the WebView
* will continue to load the resource as usual. Otherwise, the return
* response and data will be used. NOTE: This method is called on a thread
* other than the UI thread so clients should exercise caution
* when accessing private data or the view system.
*
* @param view The {@link android.webkit.WebView} that is requesting the
* resource.
* @param request Object containing the details of the request.
* @return A {@link android.webkit.WebResourceResponse} containing the
* response information or null if the WebView should load the
* resource itself.
*/
public WebResourceResponse shouldInterceptRequest(WebView view,
WebResourceRequest request) {
return shouldInterceptRequest(view, request.getUrl().toString());
}
大致翻译:
当webview页面有资源请求的时候通知宿主应用,允许应用自己返回数据给webview。如果返回值是null,就正常加载返回的数据,否则就加载应用自己return的response给webview。注意,这个方法回调在子线程而不是UI线程所以在操作私有数据或者view视图的时候要小心。
通常这个方法是用来监控所有的页面请求的,可以用它来监控黑名单以防止页面劫持,当怀疑域名被劫持时,可以通过本地http请求代理,然后将结果返回给webview。下面是我的代码的简化
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
try {
//走本地代理
InputStream is;
if(isHocked) {
ResponseBody httpResponse = HttpProxyClient.execute(request);
is = httpResponse.byteStream();
retrun new WebResourceResponse(httpResponse.contentType().type() + "/" + httpResponse.contentType().subtype(),
responseBody.contentType().charset().displayName(), is)
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(is!=null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
然而当我走到这段代码,网页无一例外,都显示了出错,走到了错误占位页。方的不行,以为是网络请求出了问题,debug跟到httpResponseBody,发现response code为200。但是再往下走,返回了WebResourceResponse之后,仍然走到了错误页。我百思不得其解。打了一堆日志也没有发现有什么问题。
折腾了一天,一筹莫展。偶然的我把日志换成Verbose并且去掉所有过滤,看到了这么一行:
E/InputStreamUtil: Got exception when calling available() on an InputStream returned from shouldInterceptRequest. This will cause the related request to fail.
说是调用从shouldInterceptRequest返回的inputStream 调用available()发生异常,会导致相关的请求失败。看了下这个方法的说明:
/**
* Returns an estimate of the number of bytes that can be read (or
* skipped over) from this input stream without blocking by the next
* invocation of a method for this input stream. The next invocation
* might be the same thread or another thread. A single read or skip of this
* many bytes will not block, but may read or skip fewer bytes.
*
* <p> Note that while some implementations of {@code InputStream} will return
* the total number of bytes in the stream, many will not. It is
* never correct to use the return value of this method to allocate
* a buffer intended to hold all data in this stream.
*
* <p> A subclass' implementation of this method may choose to throw an
* {@link IOException} if this input stream has been closed by
* invoking the {@link #close()} method.
*
* <p> The {@code available} method for class {@code InputStream} always
* returns {@code 0}.
*
* <p> This method should be overridden by subclasses.
*
* @return an estimate of the number of bytes that can be read (or skipped
* over) from this input stream without blocking or {@code 0} when
* it reaches the end of the input stream.
* @exception IOException if an I/O error occurs.
*/
public int available() throws IOException {
return 0;
}
这个方法是被子类重写的,官方的建议是如果inputstream的close被调用,子类重写时可以在里面抛IO异常。到这里就有些眉目了,有可能是httpResponse.byteStream()返回的inputStream类型,它的available()实现是如果调用了close就抛出异常。而我的代码里在finally中确实调用了close导致的。那么httpResponse.byteStream()返回的inputStream类型是不是真的就是这么实现的呢?byteStream()返回的是BufferSource的inputStream(),
我用的是Okhttp3,跟踪代码发现它使用的是RealBufferSource,继承了BufferSource。它的inputStream 方法的实现:
public InputStream inputStream() {
return new InputStream() {
public int read() throws IOException {
if(RealBufferedSource.this.closed) {
throw new IOException("closed");
} else {
if(RealBufferedSource.this.buffer.size == 0L) {
long count = RealBufferedSource.this.source.read(RealBufferedSource.this.buffer, 8192L);
if(count == -1L) {
return -1;
}
}
return RealBufferedSource.this.buffer.readByte() & 255;
}
}
public int read(byte[] data, int offset, int byteCount) throws IOException {
if(RealBufferedSource.this.closed) {
throw new IOException("closed");
} else {
Util.checkOffsetAndCount((long)data.length, (long)offset, (long)byteCount);
if(RealBufferedSource.this.buffer.size == 0L) {
long count = RealBufferedSource.this.source.read(RealBufferedSource.this.buffer, 8192L);
if(count == -1L) {
return -1;
}
}
return RealBufferedSource.this.buffer.read(data, offset, byteCount);
}
}
public int available() throws IOException {
if(RealBufferedSource.this.closed) {
throw new IOException("closed");
} else {
return (int)Math.min(RealBufferedSource.this.buffer.size, 2147483647L);
}
}
public void close() throws IOException {
RealBufferedSource.this.close();
}
public String toString() {
return RealBufferedSource.this + ".inputStream()";
}
};
}
可以看到确实available()里面如果调用过close,会抛出IO异常。到此,这个谜题就都解开了。
我把finally中的close操作去掉,果然就能正常打开页面了。
总结
这个问题查了好久没找到原因,是因为对这个机制不熟悉,理解不够充分。从写法上看,在finally里面关闭流是最正常不过的事情了,但是关了就会导致所有走代理的请求web view全都不认。仔细想想,为什么WebResourceResponse的构造入参要有这个么个东西?因为app代理的response要交给webview处理,webview要拿到response里的内容,要么直接丢给它一个Response类型,但是不同的http工具定义的response不是一个东西,webview应该对客户端自己用什么http方式不关心的,所以它只拿了一个inputstream的流。