如何获取WebViewClient.shouldInterceptRequest异步调用 [英] How to get WebViewClient.shouldInterceptRequest invoked asynchronously

查看:193
本文介绍了如何获取WebViewClient.shouldInterceptRequest异步调用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I want to create an Intranet-Application. This app is going to show content, normally only reachable in our internal environment. e.g. http://intranet.ourfirm.com

现在,我们可以从外部访问此内容 例如 https://ourproxy.com/ourIntranetApplicationID/(这将定向到

Now we are having the possibility to access this content from external e.g. https://ourproxy.com/ourIntranetApplicationID/ (this will be directed to http://intranet.ourfirm.com)

我更改了每个原始网址,例如 http://intranet.ourfirm.com/whatever/index. html https://ourproxy.com/ourIntranetApplicationID/whatever/index.html .

I change every original url like http://intranet.ourfirm.com/whatever/index.html to https://ourproxy.com/ourIntranetApplicationID/whatever/index.html.

在index.htm中,以绝对或相对方式定义了几种资源. 我将它们全部设置为绝对,然后将它们转换为我们的代理URL(请参阅* 1)(可以从公司外部的任何地方访问)

In the index.htm several resources are defined either in an absolute or relative way. I make them all absolute and convert them to our proxy url(see *1 ) (reachable from everywhere outside our firm)

这一切都工作得很好,但是有一个大问题.像地狱一样慢! 转换过程是在MyWebViewClient.shouldInterceptRequest方法中启动的.

This all is working perfectly, but with one big issue. It is slow like hell! the process of conversion is initiated in my MyWebViewClient.shouldInterceptRequest method.

我的html有80个资源要加载,并且应该为每个资源依次调用shouldInterceptRequest:

my html has 80 Resources to be loaded and shouldInterceptRequest is called sequentially for each resource:

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递增和记录, 并在方法末尾记录该值.

As you can see, I use an AtomicInteger incrementing and logging at beginning of the interception method, and log the value at the end of the method.

它总是记录:

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()方法永远不会异步启动. 如果该方法将被异步调用,则将产生一个更大的数字@ ENTER-将在出现先前数字的LEAVE之前出现注释. 不幸的是,这从未发生过.

With this I was able to check that the shouldInterceptRequest() method never get startet asynchonously. If the method would get called asynchronously a bigger number @ ENTER- Comment would appear before a LEAVE of the prior number would occure. This unfortunately never happened.

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

The call to myRewritingHelper.getConnection() is non-locking.

现在我的问题是: 是否有可能引起WebviewClient异步调用其shouldInterceptRequest()方法? 我很确定,如果可以异步加载Web视图的多个资源,这将大大提高性能! Web视图依次加载资源.

Now my Question: Is there a possibility to provoke the WebviewClient to call its shouldInterceptRequest() method asynchonously? I'm quite sure this would massively improove the performance, if several resources of the Web view could be loaded asynchonously! The Web view loads resource after resource sequentially.

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

An interesting sub-question is, why i have to define the mime-type in the Creation of the Web Resource to 0 (see *2). A call like... response = new WebResourceResponse(mime, charset, isContents); ... doesn't work.

感谢您提供有用的答案

myRewritingHelper.getConnection(..)的方法快速,它只是打开带有附加的HTTP标头的连接:

The method of myRewritingHelper.getConnection(..) is fast, it simply opens the connection with appended http headers:

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(..)方法仅消耗几毫秒.

The getConnection(..) method only consumes a couple of milliseconds.

ShouldInterceptRequest方法中最重要的瓶颈"是注释后的3个调用//读取输入

The great "bottleneck" in the shouldInterceptRequest method are the 3 calls after the comment // Read input

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

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

Those 3 calls consume up to 2Seconds each time. So the shouldInterceptRequestMethod() consumes more than 2 seconds each call.(That was the reason I asked to invoke this method asynchronously)

Mikhail Naganov建议进行预取.谁能展示一个示例,说明如何预取并将数据正确地提供给WebResourceResponse?

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

if I create the WebResourceResponse with the real mime -type instead of null (see *2) then the content cant be loaded. An html/text will be displayed as text in the WebView.

米哈伊尔(Mikhail)提出的建议解决方案似乎是正确的. 但不幸的是,不是:

Edited 2: The suggested solution from Mikhail seemed to be the right one. But unfortunately it is not:

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是这样的:

The MyWebResourceInputStream is like this:

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(),因此getEncoding始终都为null. 我开始使用预定义的编码(取决于url)定义一种解决方法.但这与通用解决方案相去甚远! 有人知道替代解决方案吗?对不起,米哈伊尔(Mikhail)拿走了公认的答案.

it brought a performance boost, but it has the huge drawback that the encoding is not defined during creation of the MyWebResourceResponse class. Because the connection gets established not until MyWebResourceInputStream.read() is called. I have discovered that the webkit calls getEncoding() prior to getData(), when the connection is not established, so it all the time getEncoding will be null. I started to define a Workaround with a predefined encoding (depending on the url ). But that's far away from a generic solution!And does not work in each case Does anybody known an alternative solution? Sorry Mikhail for taking away the accepted answer.

推荐答案

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

The resource loading process consists of two phases: creating request jobs, and then running them for getting the data. shouldInterceptRequest is called during the first phase, and these calls indeed run on a single thread, in sequence. But as the WebView's resource loader receives the request jobs, it then starts to load the resource contents from the provided streams in parallel.

创建请求作业应该很快,并且不应该成为瓶颈.您实际上测量了shouldInterceptRequest完成所需的时间吗?

Creating request jobs should be fast, and it shouldn't be a bottleneck. Did you actually measure how long does it take for your shouldInterceptRequest to complete?

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

The next step would be to check that the input streams are actually not blocking each other. Also, does RewritingHelper pre-fetch the contents, or does it only loads them on demand when the stream is being read? Pre-fetching can help increasing loading speed.

关于mime类型-通常浏览器从响应头中获取它,这就是为什么需要它通过WebResourceResponse构造函数提供它的原因.我实际上不确定您的注释中的"WebResourceResponse直接从流中获取类型"是什么意思-流仅包含答复的数据,但不包含响应标头.

As for the mime type -- usually browsers get it from the response headers, and that's why it is needed to provide it via WebResourceResponse constructor. I'm actually not sure what do you mean by "WebResourceResponse gets the type directly from the stream" in your comment -- the stream only contains the data of the reply, but not the response headers.

更新

因此,从您更新的问题来看,似乎HttpURLConnection实际上是加载shouldInterceptRequest内部的资源,这就是为什么一切如此缓慢的原因.您需要做的是定义自己的包装WebResourceResponse的类,并且在构造上不执行任何操作,因此shouldInterceptRequest可以快速执行.实际的加载应在此之后开始.

So, from your updated question it seems that HttpURLConnection actually does loading of the resource inside shouldInterceptRequest, which is why everything is so slow. What you need to do instead is to define your own class that wraps WebResourceResponse and does nothing on the construction, so shouldInterceptRequest executes fast. The actual loading should start afterwards.

我找不到很多关于此技术的好的代码示例,但是这个似乎正在或多或少地满足您的要求:

I couldn't find a lot of good code examples for this technique, but this one seems to be doing more or less what you need: https://github.com/mobilyzer/Mobilyzer/blob/master/Mobilyzer/src/com/mobilyzer/util/AndroidWebView.java#L252

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

By pre-fetching I mean that you can start loading your data almost immediately after you have returned from shouldInterceptRequest, not waiting until WebView calls getData method on the returned WebResourceResponse. That way, you will already have the data loaded by the time WebView asks you.

更新2

这实际上是WebView中的问题,它从shouldInterceptRequest接收到WebResourceResponse的实例后立即查询响应标头.这意味着,如果应用程序要从网络本身加载资源(例如用于修改资源),则加载速度将永远不会像WebView自身加载资源时那样快.

It's actually a problem in WebView that it queries the response headers immediately after receiving the instance of WebResourceResponse from shouldInterceptRequest. It means that if the app wants to load resources from the network itself (e.g. for modifying them), loading will never be as fast as when WebView loads those resources itself.

应用程序可以执行的最佳方法是这样的(代码缺少适当的异常和错误处理,否则将大3倍):

The best approach the app can do is something like this (the code lacks proper exception and error handling, otherwise it will be 3 times bigger):

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();
        }
    }
);

这篇关于如何获取WebViewClient.shouldInterceptRequest异步调用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆