1、HttpUrlSource.fetchContentInfo()
此方法作用是获取url的length(长度)和mime(文件类型),在HttpUrlSource.length()和HttpUrlSource.getMime()中被调用,而调用HttpUrlSource.length()和HttpUrlSource.getMime()则是在HttpProxyCache.newResponseHeaders()中。
public synchronized int length() throws ProxyCacheException { if (length == Integer.MIN_VALUE) { fetchContentInfo(); } return length; }
public synchronized String getMime() throws ProxyCacheException { if (TextUtils.isEmpty(mime)) { fetchContentInfo(); } return mime; }
private void fetchContentInfo() throws ProxyCacheException { Log.d(LOG_TAG, "Read content info from " + url); HttpURLConnection urlConnection = null; InputStream inputStream = null; try { urlConnection = openConnection(0, 10000); length = urlConnection.getContentLength(); mime = urlConnection.getContentType(); inputStream = urlConnection.getInputStream(); Log.i(LOG_TAG, "Content info for `" + url + "`: mime: " + mime + ", content-length: " + length); } catch (IOException e) { Log.e(LOG_TAG, "Error fetching info from " + url, e); } finally { ProxyCacheUtils.close(inputStream); if (urlConnection != null) { urlConnection.disconnect(); } } }
private HttpURLConnection openConnection(int offset, int timeout) throws IOException, ProxyCacheException { HttpURLConnection connection; boolean redirected; int redirectCount = 0; String url = this.url; do { Log.d(LOG_TAG, "Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url); connection = (HttpURLConnection) new URL(url).openConnection(); if (offset > 0) { connection.setRequestProperty("Range", "bytes=" + offset + "-"); } if (timeout > 0) { connection.setConnectTimeout(timeout); connection.setReadTimeout(timeout); } int code = connection.getResponseCode(); redirected = code == HTTP_MOVED_PERM || code == HTTP_MOVED_TEMP || code == HTTP_SEE_OTHER; if (redirected) { url = connection.getHeaderField("Location"); redirectCount++; connection.disconnect(); } if (redirectCount > MAX_REDIRECTS) { throw new ProxyCacheException("Too many redirects: " + redirectCount); } } while (redirected); return connection; }
从以上代码可以看到fetchContentInfo()利用HttpURLConnection获取url的长度和文件类型,默认是"GET"方法,返回了BODY数据,在fetchContentInfo()中,BODY里面的数据根本就没有用到,并且在某些API版本中HttpURLConnection.disconnect()方法会将HttpURLConnection中的数据流读完才会关闭,耗时数秒甚至更长(取决于url指向的文件大小)。而url的length(长度)和mime(文件类型)这两个值是存在Http响应的头部,那么可以使用Http的“HEAD”方法,只返回头部,不需要BODY,既可以提高响应速度也可以减少网络流量。只需要增加一行代码即可,但是openConnection()方法在其它地方也会用到,因此应该单独为fetchContentInfo()写一个openConnection()方法,如openConnectionForHeader():
private HttpURLConnection openConnectionForHeader(int timeout) throws IOException, ProxyCacheException { HttpURLConnection connection; boolean redirected; int redirectCount = 0; String url = this.url; do { VideoCacheLog.d(LOG_TAG, "Open connection for header to " + url); connection = (HttpURLConnection) new URL(url).openConnection(); if (timeout > 0) { connection.setConnectTimeout(timeout); connection.setReadTimeout(timeout); } connection.setRequestMethod("HEAD"); int code = connection.getResponseCode(); redirected = code == HTTP_MOVED_PERM || code == HTTP_MOVED_TEMP || code == HTTP_SEE_OTHER; if (redirected) { url = connection.getHeaderField("Location"); VideoCacheLog.d(LOG_TAG, "Redirect to:" + url); redirectCount++; connection.disconnect(); VideoCacheLog.d(LOG_TAG, "Redirect closed:" + url); } if (redirectCount > MAX_REDIRECTS) { throw new ProxyCacheException("Too many redirects: " + redirectCount); } } while (redirected); return connection; }
2、一般情况下,url对应的长度和文件类型是不会变化的,因此将url的length(长度)和mime(文件类型)加入到缓存,不用每次都打开HttpURLConnection。增加IMimeCache类和UrlMime类:
public interface IMimeCache { public void putMime(String url,int length,String mime); public UrlMime getMime(String url); }
public class UrlMime { protected int length = Integer.MIN_VALUE; protected String mime; public UrlMime(){ } public int getLength() { return length; } public void setLength(int length) { this.length = length; } public String getMime() { return mime; } public void setMime(String mime) { this.mime = mime; } }
由HttpProxyCacheServer实现此接口,利用HashMap保存数据,并通过HttpProxyCacheServerClients的构造函数注入IMimeCache,在HttpProxyCacheServerClients.newHttpProxyCache()方法中通过HttpUrlSource的构造函数注入IMimeCache,最后在fetchContentInfo()调用之后将数据缓存,以便下次直接获取缓存。
private void fetchContentInfo() throws ProxyCacheException { VideoCacheLog.d(LOG_TAG, "Read content info from " + url); HttpURLConnection urlConnection = null; InputStream inputStream = null; try { urlConnection = openConnectionForHeader(10000); length = urlConnection.getContentLength(); mime = urlConnection.getContentType(); //将数据放入缓存 tryPutMimeCache(); inputStream = urlConnection.getInputStream(); VideoCacheLog.i(LOG_TAG, "Content info for `" + url + "`: mime: " + mime + ", content-length: " + length); } catch (IOException e) { VideoCacheLog.e(LOG_TAG, "Error fetching info from " + url, e); } finally { ProxyCacheUtils.close(inputStream); if (urlConnection != null) { urlConnection.disconnect(); urlConnection = null ; } VideoCacheLog.d(LOG_TAG, "Closed connection from :" + url); } }
public synchronized String getMime() throws ProxyCacheException { if (TextUtils.isEmpty(mime)) { tryLoadMimeCache(); } if (TextUtils.isEmpty(mime)) { fetchContentInfo(); } return mime; }
public synchronized int length() throws ProxyCacheException { if (length == Integer.MIN_VALUE) { tryLoadMimeCache(); } if (length == Integer.MIN_VALUE) { fetchContentInfo(); } return length; }
private void tryLoadMimeCache(){ if(mimeCache!=null){ UrlMime urlMime = mimeCache.getMime(url); if(urlMime!=null && !TextUtils.isEmpty(urlMime.getMime()) && urlMime.getLength()!=Integer.MIN_VALUE){ this.mime = urlMime.getMime(); this.length = urlMime.getLength() ; } } }
private void tryPutMimeCache(){ if(mimeCache!=null){ mimeCache.putMime(url,length,mime); } }
3、HttpProxyCache.responseWithoutCache()方法中的bug
对于同一个url,AndroidVideoCache使用同一个HttpProxyCacheServerClients实例,
private HttpProxyCacheServerClients getClients(String url) throws ProxyCacheException { synchronized (clientsLock) { HttpProxyCacheServerClients clients = clientsMap.get(url); if (clients == null) { clients = new HttpProxyCacheServerClients(this,url, config); clientsMap.put(url, clients); } return clients; } }
在HttpProxyCacheServerClients中共享着同一个HttpProxyCache实例,
public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException { startProcessRequest(); try { clientsCount.incrementAndGet(); proxyCache.processRequest(request, socket); } finally { finishProcessRequest(); } }
private synchronized void startProcessRequest() throws ProxyCacheException { proxyCache = proxyCache == null ? newHttpProxyCache() : proxyCache; }
private HttpProxyCache newHttpProxyCache() throws ProxyCacheException { HttpUrlSource source = new HttpUrlSource(url); FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage); HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache); httpProxyCache.registerCacheListener(uiCacheListener); return httpProxyCache; }
当MediaPlayer有多个请求时,HttpProxyCache.processRequest()方法会分别运行在多个线程中,在HttpProxyCache.responseWithoutCache()方法的finally语句块则会导致其他线程ProxyCache.readSource()方法出错。
private void responseWithoutCache(OutputStream out, long offset) throws ProxyCacheException, IOException { try { HttpUrlSource source = new HttpUrlSource(this.source); source.open((int) offset); byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; int readBytes; while ((readBytes = source.read(buffer)) != -1) { out.write(buffer, 0, readBytes); offset += readBytes; } out.flush(); } finally { source.close(); } }
情况是:如果此时有另外一个线程执行的是HttpProxyCache.responseWithCache(),
private void responseWithCache(OutputStream out, long offset) throws ProxyCacheException, IOException { byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; int readBytes; while ((readBytes = read(buffer, offset, buffer.length)) != -1) { out.write(buffer, 0, readBytes); offset += readBytes; } out.flush(); }
那么就会进入到HttpProxyCache的父类ProxyCache.read(byte[] buffer, long offset, int length)方法中,
public int read(byte[] buffer, long offset, int length) throws ProxyCacheException { ProxyCacheUtils.assertBuffer(buffer, offset, length); while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped) { readSourceAsync(); waitForSourceData(); checkReadSourceErrorsCount(); } int read = cache.read(buffer, offset, length); if (cache.isCompleted() && percentsAvailable != 100) { percentsAvailable = 100; onCachePercentsAvailableChanged(100); } return read; }
然后会执行到ProxyCache.readSource()方法中,
private void readSource() { int sourceAvailable = -1; int offset = 0; try { offset = cache.available(); source.open(offset); sourceAvailable = source.length(); byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE]; int readBytes; while ((readBytes = source.read(buffer)) != -1) { synchronized (stopLock) { if (isStopped()) { return; } cache.append(buffer, readBytes); } offset += readBytes; notifyNewCacheDataAvailable(offset, sourceAvailable); } tryComplete(); } catch (Throwable e) { readSourceErrorsCount.incrementAndGet(); onError(e); } finally { closeSource(); notifyNewCacheDataAvailable(offset, sourceAvailable); } }
而此时HttpProxyCache.responseWithoutCache()方法中的source.close()则会导致父类ProxyCache中的source被close,从而导致ProxyCache.readSource()方法抛出异常,进而导致MediaPlayer的一个请求线程出错,最后的结果是MediaPlayer出错,触发OnErrorListener。解决办法则是修改HttpProxyCache.responseWithoutCache()方法,关闭try语句块中的source。
4、由于HttpURLConnection.disconnect()耗时太久导致HttpUrlSource.fetchContentInfo()耗时过长
使用了"HEAD"方法代替"GET"方法后,在一些Android手机上HttpURLConnection.disconnect()方法仍然耗时太久,进行导致MediaPlayer要等待很久才会开始播放,因此决定使用okhttp替换HttpURLConnection。
增加一个抽象类UrlSource继承Source类,使之前的HttpUrlSource和新增的OkHttpSource继承UrlSource,这样使得HttpURLConnection和okhttp同时存在,并且可以随时切换。
public abstract class UrlSource implements Source{ protected String url; protected volatile int length = Integer.MIN_VALUE; protected volatile String mime; public UrlSource(String url){ this.url = url ; } public UrlSource(UrlSource urlSource){ this.url = urlSource.url; this.length = urlSource.length; this.mime = urlSource.mime ; } public abstract String getMime() throws ProxyCacheException; @Override public String toString() { return "UrlSource{" + "url='" + url + '\'' + ", length=" + length + ", mime='" + mime + '\'' + '}'; } }
OkHttpSource代码如下:
public class OkHttpSource extends UrlSource{ private static final int MAX_REDIRECTS = 5; // private IMimeCache mimeCache ; private OkHttpClient httpClient = new OkHttpClient(); private InputStream inputStream; public OkHttpSource(IMimeCache mimeCache,String url) { super(url); this.mimeCache = mimeCache ; } public OkHttpSource(UrlSource urlSource){ super(urlSource); } @Override public int length() throws ProxyCacheException { if (length == Integer.MIN_VALUE) { tryLoadMimeCache(); } if (length == Integer.MIN_VALUE) { fetchContentInfo(); } return length; } @Override public String getMime() throws ProxyCacheException { if (TextUtils.isEmpty(mime)) { tryLoadMimeCache(); } if (TextUtils.isEmpty(mime)) { fetchContentInfo(); } return mime; } @Override public void open(int offset) throws ProxyCacheException { try { Response response = openConnection(offset, -1); mime = response.body().contentType().toString(); length = readSourceAvailableBytes(response, offset); inputStream = new BufferedInputStream(response.body().byteStream(), DEFAULT_BUFFER_SIZE); } catch (IOException e) { throw new ProxyCacheException("Error opening connection for " + url + " with offset " + offset, e); } } private int readSourceAvailableBytes(Response response, int offset) throws IOException { int responseCode = response.code() ; int contentLength = (int) response.body().contentLength(); return responseCode == HTTP_OK ? contentLength : responseCode == HTTP_PARTIAL ? contentLength + offset : length; } @Override public int read(byte[] buffer) throws ProxyCacheException { if (inputStream == null) { throw new ProxyCacheException("Error reading data from " + url + ": connection is absent!"); } try { return inputStream.read(buffer, 0, buffer.length); } catch (InterruptedIOException e) { throw new InterruptedProxyCacheException("Reading source " + url + " is interrupted", e); } catch (IOException e) { throw new ProxyCacheException("Error reading data from " + url, e); } } @Override public void close() throws ProxyCacheException { ProxyCacheUtils.close(inputStream); } private void fetchContentInfo() throws ProxyCacheException { VideoCacheLog.d(LOG_TAG, "Read content info from " + url); Response response = null ; try { response = openConnectionForHeader(10000); if(response==null || !response.isSuccessful()){ throw new ProxyCacheException("Fail to fetchContentInfo: " + url); } length = (int) response.body().contentLength(); mime = response.body().contentType().toString(); tryPutMimeCache(); VideoCacheLog.i(LOG_TAG, "Content info for `" + url + "`: mime: " + mime + ", content-length: " + length); } catch (IOException e) { VideoCacheLog.e(LOG_TAG, "Error fetching info from " + url, e); } finally { VideoCacheLog.d(LOG_TAG, "Closed connection from :" + url); } } private Response openConnectionForHeader(int timeout) throws IOException, ProxyCacheException { if(timeout>0){ httpClient.setConnectTimeout(timeout, TimeUnit.MILLISECONDS); httpClient.setReadTimeout(timeout, TimeUnit.MILLISECONDS); httpClient.setWriteTimeout(timeout, TimeUnit.MILLISECONDS); } Response response ; boolean isRedirect = false; String newUrl = this.url ; int redirectCount = 0; do { Request request = new Request.Builder() .head() .url(newUrl) .build(); response = httpClient.newCall(request).execute(); if(response.isRedirect()){ newUrl = response.header("Location"); isRedirect = response.isRedirect() ; redirectCount++; } if(redirectCount>MAX_REDIRECTS){ throw new ProxyCacheException("Too many redirects: " + redirectCount); } }while (isRedirect); return response ; } private Response openConnection(int offset,int timeout) throws IOException, ProxyCacheException { if(timeout>0){ httpClient.setConnectTimeout(timeout, TimeUnit.MILLISECONDS); httpClient.setReadTimeout(timeout, TimeUnit.MILLISECONDS); httpClient.setWriteTimeout(timeout, TimeUnit.MILLISECONDS); } Response response ; boolean isRedirect = false; String newUrl = this.url ; int redirectCount = 0; do { VideoCacheLog.d(LOG_TAG, "Open connection" + (offset > 0 ? " with offset " + offset : "") + " to " + url); Request.Builder requestBuilder = new Request.Builder(); requestBuilder.get(); requestBuilder.url(newUrl); if (offset > 0) { requestBuilder.addHeader("Range", "bytes=" + offset + "-"); } response = httpClient.newCall(requestBuilder.build()).execute(); if(response.isRedirect()){ newUrl = response.header("Location"); isRedirect = response.isRedirect() ; redirectCount++; } if(redirectCount>MAX_REDIRECTS){ throw new ProxyCacheException("Too many redirects: " + redirectCount); } }while (isRedirect); return response ; } private void tryLoadMimeCache(){ if(mimeCache!=null){ UrlMime urlMime = mimeCache.getMime(url); if(urlMime!=null && !TextUtils.isEmpty(urlMime.getMime()) && urlMime.getLength()!=Integer.MIN_VALUE){ this.mime = urlMime.getMime(); this.length = urlMime.getLength() ; } } } private void tryPutMimeCache(){ if(mimeCache!=null){ mimeCache.putMime(url,length,mime); } } }
最后将之前使用HttpUrlSource的地方替换为OkHttpSource即可。测试之后发现使用okhttp确实避免了MediaPlayer要等待很久才会开始播放的问题。