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
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)
I change every original url like http://intranet.ourfirm.com/whatever/index.html to https://ourproxy.com/ourIntranetApplicationID/whatever/index.html.
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)
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.
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;
}
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.
It always logs:
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
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.
The call to myRewritingHelper.getConnection() is non-locking.
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.
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.
Thanks for any helpful answers
Edited:
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;
}
The getConnection(..) method only consumes a couple of milliseconds.
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();
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 suggested to do a pre-fetch. Can anybody show an example of how to prefetching and give the data properly to the WebResourceResponse?
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.
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();
}
}
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();
}
the call is initiated in
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);
}
}
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.
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.
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?
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.
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.
UPDATE
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
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.
UPDATE 2
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.
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();
}
}
);