我想创建一个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();
}
}
);