记一次webview 中使用shouldInterceptRequest的踩坑

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的流。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值