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