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

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

我将每个原始网址(如http://intranet.ourfirm.com/whatever/index.html)更改为
https://ourproxy.com/ourIntranetApplicationID/whatever/index.html

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

所有这些都运行良好,但是存在一个大问题。像 hell 一样慢!
转换过程在MyWebViewClient.shouldInterceptRequest方法中启动。

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

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

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

现在我的问题:
是否有可能激发WebviewClient异步调用其shouldInterceptRequest()方法?
我很确定,如果可以异步加载Web View 的多个资源,这将大大提高性能!
Web View 按资源顺序加载资源。


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

感谢您提供任何有用的答案

编辑:

myRewritingHelper.getConnection(..)的方法非常快,它只是打开带有附加的HTTP header 的连接:
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秒(这就是我要求异步调用此方法的原因)

Mikhail Naganov建议进行预提取。任何人都可以显示一个示例,说明如何预取并将数据正确地提供给WebResourceResponse吗?

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

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

最佳答案

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

创建请求作业应该很快,并且不应该成为瓶颈。您实际上是否测量了shouldInterceptRequest需要多长时间?

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

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

更新

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

我找不到很多适合此技术的良好代码示例,但该示例似乎在或多或少地满足了您的要求:https://github.com/mobilyzer/Mobilyzer/blob/master/Mobilyzer/src/com/mobilyzer/util/AndroidWebView.java#L252

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

更新2

在WebView中,实际上这是一个问题,它从WebResourceResponse接收到shouldInterceptRequest实例后立即查询响应头。这意味着,如果应用程序要从网络本身加载资源(例如,修改资源),则加载速度将永远不会像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();
        }
    }
);

10-07 19:35
查看更多