如何异步调用WebViewClient.sshouldInterceptRequest



我想创建一个Intranet应用程序。此应用程序将显示内容,通常只能在我们的内部环境中访问。例如。http://intranet.ourfirm.com

现在我们有可能从外部访问这些内容例如。https://ourproxy.com/ourIntranetApplicationID/(将发送至http://intranet.ourfirm.com)

我更改每个原始urlhttp://intranet.ourfirm.com/whatever/index.html到https://ourproxy.com/ourIntranetApplicationID/whatever/index.html.

在index.htm中,以绝对或相对的方式定义了几个资源。我让它们都是绝对的,并将它们转换为我们的代理url(见*1)(从我们公司以外的任何地方都可以访问)

这一切都很完美,但有一个大问题。太慢了!转换过程是在我的MyWebViewClient.sshouldInterceptRequest方法中启动的。

我的html有80个资源要加载,并且应该为每个资源顺序调用interceptRequest:

public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        LOGGER.debug("ENTER shouldInterceptRequest: " + String.format("%012d", interceptCounter.incrementAndGet()));
        WebResourceResponse response;

    HttpURLConnection conn;
        try {
            conn = myRewritingHelper.getConnection(request.getUrl(), method); // *1 this internally converts the url and gets a connection adds the header for Basic Auth etc.
            // add request headers
            for (Map.Entry<String, String> entry : request.getRequestHeaders().entrySet()) {
                conn.setRequestProperty(entry.getKey(), entry.getValue());
            }
            // Read input
            String charset = conn.getContentEncoding() != null ? conn.getContentEncoding() : Charset.defaultCharset().displayName();
            String mime = conn.getContentType();
            InputStream is = conn.getInputStream();

            long interceptStopTimestamp = System.currentTimeMillis();
            long durationIntercepting = interceptStopTimestamp - interceptStartTimestamp;
            LOGGER.info("InterceptionDuration : " + durationIntercepting);
            // *2 we have to define null for the mime-type , so the WebResourceResponse gets the type directly from the stream
            response = new WebResourceResponse(null, charset, isContents);
        } catch (IllegalStateException e) {
            LOGGER.warn("IllegalStateException", e);
        } catch (IOException e) {
            LOGGER.warn("IOException: Could not load resource: " + url, e);
        }

        LOGGER.debug("LEAVE shouldInterceptRequest: " + String.format("%012d", interceptCounter.get()));
        return response;
    }

正如您所看到的,我在拦截方法的开头使用了AtomicInteger递增和日志记录,并在方法结束时记录该值。

它总是记录:

ENTER shouldInterceptRequest:  000000000001
LEAVE shouldInterceptRequest:  000000000001
ENTER shouldInterceptRequest:  000000000002
LEAVE shouldInterceptRequest:  000000000002
ENTER shouldInterceptRequest:  000000000003
LEAVE shouldInterceptRequest:  000000000003
ENTER shouldInterceptRequest:  000000000004
LEAVE shouldInterceptRequest:  000000000004
:
:
ENTER shouldInterceptRequest:  000000000080
LEAVE shouldInterceptRequest:  000000000080

有了这个,我可以检查shouldInterceptRequest()方法是否从未异步地获得startet。如果异步调用该方法,则在出现前一个数字的LEAVE之前,将出现一个较大的数字@ENTER-Comment。不幸的是,这种情况从未发生过。

对myRewriteingHelper.getConnection()的调用是非锁定的。

现在我的问题是:是否有可能促使WebviewClient异步调用其shouldInterceptRequest()方法?我确信,如果Web视图的几个资源可以异步加载,这将大大提高性能!Web视图依次加载一个又一个资源

一个有趣的子问题是,为什么我必须将Web资源创建中的mime类型定义为0(请参阅*2)。一个像。。。response=新的WebResourceResponse(mime,charset,isContents);…不起作用。

感谢任何有用的答案

编辑:

myRewriteingHelper.getConnection(..)的方法很快,它只需打开带有附加http头的连接:

private HttpURLConnection getConnection(String url, String httpMethod) throws MalformedURLException, IOException {
        String absoluteRewrittenUrl = urlConfigurationManager.getRewritedUrl(url); // this gets a rewritten url
        final HttpURLConnection connection = (HttpURLConnection) new URL(absoluteRewrittenUrl).openConnection();
        connection.setRequestMethod(httpMethod);
        connection.setConnectTimeout(CONNECTION_TIMEOUT_MS);
        connection.setReadTimeout(SOCKET_TIMEOUT_MS);
        connection.setRequestProperty("AUTHORIZATION",getBasicAuthentication());
        return connection;
    }

getConnection(..)方法只消耗几毫秒。

shouldInterceptRequest方法中最大的"瓶颈"是注释//读取输入之后的3个调用

String charset = conn.getContentEncoding() != null
conn.getContentEncoding():Charset.defaultCharset().displayName();
String mime = conn.getContentType();
InputStream is = conn.getInputStream();

这3个呼叫每次最多消耗2秒。因此,shouldInterceptRequestMethod()每次调用消耗的时间超过2秒。(这就是我要求异步调用此方法的原因)

米哈伊尔·纳加诺夫建议进行预取。有人能举例说明如何预取数据并将数据正确地提供给WebResourceResponse吗

如果我用真正的mime-type而不是null创建WebResourceResponse(请参阅*2),那么内容将无法加载。html/文本将在WebView中显示为文本。

编辑2:米哈伊尔提出的解决方案似乎是正确的。但不幸的是,它不是:

public class MyWebResourceResponse extends WebResourceResponse {
    private String url;
    private Context context;
    private MyResourceDownloader myResourceDownloader;
    private String method;
    private Map<String, String> requestHeaders;
    private MyWebViewListener myWebViewListener;
    private String predefinedEncoding;
public MyWebResourceResponse(Context context, String url, MyResourceDownloader myResourceDownloader, String method, Map<String, String> requestHeaders, MyWebViewListener myWebViewListener,String predefinedEncoding) {
        super("", "", null);
        this.url = url;
        this.context = context;
        this.myResourceDownloader = myResourceDownloader;
        this.method = method;
        this.requestHeaders = requestHeaders;
        this.myWebViewListener = myWebViewListener;
        this.predefinedEncoding = predefinedEncoding;
    }
    @Override
    public InputStream getData() {
        return new MyWebResourceInputStream(context, url, myResourceDownloader, method, requestHeaders, myWebViewListener);
    }
    @Override
    public String getEncoding() {
        if(predefinedEncoding!=null){
            return predefinedEncoding;
        }
        return super.getEncoding();
    }
    @Override
    public String getMimeType() {
        return super.getMimeType();
    }
}

MyWebResourceInputStream是这样的:

public class MyWebResourceInputStream extends InputStream {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyWebResourceInputStream.class);
    public static final int NO_MORE_DATA = -1;
    private String url;
    private boolean initialized;
    private InputStream inputStream;
    private MyResourceDownloader myResourceDownloader;
    private String method;
    private Map<String, String> requestHeaders;
    private Context context;
    private MyWebViewListener myWebViewListener;
public MyWebResourceInputStream(Context context, String url, MyResourceDownloader myResourceDownloader,
            String method, Map<String, String> requestHeaders, MyWebViewListener myWebViewListener) {
        this.url = url;
        this.initialized = false;
        this.myResourceDownloader = myResourceDownloader;
        this.method = method;
        this.requestHeaders = requestHeaders;
        this.context = context;
        this.myWebViewListener = myWebViewListener;
    }
@Override
    public int read() throws IOException {
        if (!initialized && !MyWebViewClient.getReceived401()) {
            LOGGER.debug("- -> read ENTER *****");
            try {
                InterceptingHelper.InterceptingHelperResult result = InterceptingHelper.getStream(context, myResourceDownloader, url, method, requestHeaders, false);
                inputStream = result.getInputstream();
                initialized = true;
            } catch (final UnexpectedStatusCodeException e) {
                LOGGER.warn("UnexpectedStatusCodeException", e);
                if (e.getStatusCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
                    MyWebViewClient.setReceived401(true);
                    if (myWebViewListener != null) {
                        myWebViewListener.onReceivedUnexpectedStatusCode(e.getStatusCode());
                    }
                    LOGGER.warn("UnexpectedStatusCodeException received 401", e);
                }
            } catch (IllegalStateException e) {
                LOGGER.warn("IllegalStateException", e);
            }
        }
        if (inputStream != null && !MyWebViewClient.getReceived401()) {
            return inputStream.read();
        } else {
            return NO_MORE_DATA;
        }
    }
@Override
    public void close() throws IOException {
        if (inputStream != null) {
            inputStream.close();
        }
    }
@Override
    public long skip(long byteCount) throws IOException {
        long skipped = 0;
        if (inputStream != null) {
            skipped = inputStream.skip(byteCount);
        }
        return skipped;
    }
@Override
    public synchronized void reset() throws IOException {
        if (inputStream != null) {
            inputStream.reset();
        }
    }
@Override
    public int read(byte[] buffer) throws IOException {
        if (inputStream != null) {
            return inputStream.read(buffer);
        }
        return super.read(buffer);
    }
@Override
    public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
        if (inputStream != null) {
            return inputStream.read(buffer, byteOffset, byteCount);
        }
        return super.read(buffer, byteOffset, byteCount);
    }
 public int available() throws IOException {
        if (inputStream != null) {
            return inputStream.available();
        }
        return super.available();
    }
public synchronized void mark(int readlimit) {
        if (inputStream != null) {
            inputStream.mark(readlimit);
        }
        super.mark(readlimit);
    }
 @Override
    public boolean markSupported() {
        if (inputStream != null) {
            return inputStream.markSupported();
        }
        return super.markSupported();
    }

呼叫在中启动

MyWebViewClient extends WebViewClient{
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request){
    // a lot of other code
    String predefinedEncoding = getPredefinedEncodingFromUrl(url);
            return new MyWebResourceResponse(context, url, myResourceDownloader, method, requestHeaders, webViewListener, predefinedEncoding);
  }
}

它带来了性能提升,但它有一个巨大的缺点,即在创建MyWebResourceResponse类的过程中没有定义编码。因为只有调用MyWebResourceInputStream.read()才能建立连接。我发现当连接没有建立时,webkit在getData()之前调用getEncoding(),所以它一直都是null。我开始用预定义的编码(取决于url)定义Workaround。但这远远不是一个通用的解决方案!并且在每种情况下都不起作用有人知道替代方案吗?抱歉米哈伊尔拿走了公认的答案。

资源加载过程由两个阶段组成:创建请求作业,然后运行它们来获取数据。shouldInterceptRequest在第一阶段被调用,并且这些调用确实在单个线程上按顺序运行。但是,当WebView的资源加载器接收到请求作业时,它开始并行地从提供的流中加载资源内容。

创建请求作业应该很快,而且不应该成为瓶颈。你真的测量过你的shouldInterceptRequest需要多长时间才能完成吗?

下一步是检查输入流实际上没有相互阻塞。此外,RewriteingHelper是预取内容,还是仅在读取流时按需加载内容?预取可以帮助提高加载速度。

至于mime类型——通常浏览器从响应头中获取它,这就是为什么需要通过WebResourceResponse构造函数提供它。实际上,我不知道你在评论中所说的"WebResourceResponse直接从流中获取类型"是什么意思——流只包含回复的数据,而不包含响应头。

更新

因此,从您更新的问题来看,HttpURLConnection实际上确实加载了shouldInterceptRequest内的资源,这就是为什么一切都很慢的原因。相反,您需要做的是定义自己的类,该类封装WebResourceResponse,而不对构造执行任何操作,因此shouldInterceptRequest执行速度很快。实际装载应随后开始。

对于这种技术,我找不到很多好的代码示例,但这一个似乎或多或少地满足了您的需求:https://github.com/mobilyzer/Mobilyzer/blob/master/Mobilyzer/src/com/mobilyzer/util/AndroidWebView.java#L252

通过预取,我的意思是,您几乎可以在从shouldInterceptRequest返回后立即开始加载数据,而不用等到WebView对返回的WebResourceResponse调用getData方法。这样,在WebView要求您加载数据时,您就已经加载了数据。

更新2

WebView在从shouldInterceptRequest接收到WebResourceResponse的实例后立即查询响应标头,这实际上是一个问题。这意味着,如果应用程序想从网络本身加载资源(例如修改资源),加载速度永远不会像WebView本身加载这些资源时那么快。

该应用程序能做的最好的方法是这样的(代码缺乏适当的异常和错误处理,否则它会大3倍):

public WebResourceResponse shouldInterceptRequest(WebView view, final WebResourceRequest request) {
    final CountDownLatch haveHeaders = new CountDownLatch(1);
    final AtomicReference<Map<String, String>> headersRef = new AtomicReference<>();
    final CountDownLatch haveData = new CountDownLatch(1);
    final AtomicReference<InputStream> inputStreamRef = new AtomicReference<>();
    new Thread() {
        @Override
        public void run() {
            HttpURLConnection urlConnection =
                (HttpURLConnection) new URL(request.getUrl().toString()).openConnection();
            Map<String, List<String>> rawHeaders = urlConnection.getHeaderFields();
            // Copy headers from rawHeaders to headersRef
            haveHeaders.countDown();
            inputStreamRef.set(new BufferedInputStream(urlConnection.getInputStream()));
            haveData.countDown();
        }
    }.start();
    return new WebResourceResponse(
            null,
            "UTF-8",
            new InputStream() {
               @Override
               public int read() throws IOException {
                   haveInputStream.await(100, TimeUnit.SECONDS));
                   return inputStreamRef.get().read();
            }) {
        @Override
        public Map<String, String> getResponseHeaders() {
            haveHeaders.await(100, TimeUnit.SECONDS))
            return headersRef.get();
        }
    }
);

最新更新