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