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要等待很久才会开始播放的问题。