天天看点

AndroidVideoCache源码浅析

1. 边播放边缓存

  视频播放时边播放边缓存,这样用户再次播放时可以节省流量,提高用户体验,这是视频播放很常见的需求。但是,Android的VideoView是没有提供这样的功能的。

有个开源库比较好用,github地址:https://github.com/danikula/AndroidVideoCache

2. 简述一下AndroidVideoCache的大体实现原理

  大家都知道,VideoView.setVideoPath(proxyUrl);走的也是http请求,而http底层是走tcp协议的socket实现的。AndroidVideoCache的实现就是在tcp层架一个SocketServer的代理服务器,也就是说VideoView.setVideoPath(proxyUrl);的请求是先请求到代理服务器SocketServer,然后代理服务器SocketServer再去请求真正的服务器。这样,这个代理服务器SocketServer就可以去请求真正的服务器去下载视频(AndroidVideoCache是分段下载,也就是断点续传,每次8 * 1024大小),然后将视频响应给请求。当然代理服务器SocketServer还会先判断该视频是否已经下载缓存完,如果已经下载缓存完就直接使用本地的缓存视频。

3. AndroidVideoCache源代码简述

  1)App全局架设一个本地Socket代理服务器 

InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
                this.serverSocket = new ServerSocket(0, 8, inetAddress);      

  2)getProxyUrl方法,先判断是否已经缓存过了,如果是直接使用本地缓存。如果否,就判断代理服务器SocketServer是否可用

public String getProxyUrl(String url, boolean allowCachedFileUri) {
              if (allowCachedFileUri && isCached(url)) {  //缓存ok
                    File cacheFile = getCacheFile(url);
                    touchFileSafely(cacheFile);  //touch一下文件,让该文件时间最新,用于LRUcache缓存,LRUcache缓存是按照时间排序的。
                    return Uri.fromFile(cacheFile).toString();
              }
                return isAlive() ? appendToProxyUrl(url) : url;  //isAlive()方法里就是ping了一下代理服务器SocketServer,看看是否可用?是,包装成proxyURL,否,使用来源的url,不缓存
          }      

  3)processSocket处理所有的请求进来的Socket,包括ping的和VideoView.setVideoPath(proxyUrl)的Socket

private void processSocket(Socket socket) {
                    GetRequest request = GetRequest.read(socket.getInputStream());
                    LOG.debug("Request to cache proxy:" + request);
                    String url = ProxyCacheUtils.decode(request.uri);
                    if (pinger.isPingRequest(url)) {  //如果是ping的,返回ping响应
                          pinger.responseToPing(socket);
                    } else {  //视频的Socket,启动对应的Client去处理
                          HttpProxyCacheServerClients clients = getClients(url);
                          clients.processRequest(request, socket);
                    }
      }      

  4)clients.processRequest(request, socket);方法的实现

public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException {
              startProcessRequest();  //这个方法其实就是获取一个proxyCache对应
              try {
                    clientsCount.incrementAndGet();
                    proxyCache.processRequest(request, socket);  //交给proxyCache处理
              } finally {
                    finishProcessRequest();
              }
        }
        private synchronized void startProcessRequest() throws ProxyCacheException {
              proxyCache = proxyCache == null ? newHttpProxyCache() : proxyCache;  //newHttpProxyCache() 这个方法准备一下url,缓存file路径,监听器等等。。。
        }      

  5)proxyCache.processRequest(request, socket);方法的实现

   

public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException {
              OutputStream out = new BufferedOutputStream(socket.getOutputStream());  //创建一个Socket的响应流
              String responseHeaders = newResponseHeaders(request);
              out.write(responseHeaders.getBytes("UTF-8"));
              long offset = request.rangeOffset;
              if (isUseCache(request)) {  //判断是否使用缓存?是
                    responseWithCache(out, offset);  //缓存并响应  
              } else {  //不使用缓存
                    responseWithoutCache(out, offset);  //不缓存并响应
              }
        }      

  6)responseWithCache(out, offset);方法的实现

while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped) {
                  readSourceAsync();  //异步请求真正的服务器读取该段数据,读取完会通知
                  waitForSourceData();  //加锁等待readSourceAsync()的通知
                  checkReadSourceErrorsCount();  //校验错误
            }      

  6-1)readSourceAsync();方法最后回调用readSource()方法 

private void readSource() {
              long sourceAvailable = -1;
              long offset = 0;
              try {
                    offset = cache.available();
                    source.open(offset);
                    sourceAvailable = source.length();
                    byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];  //每次读取这么多数据ProxyCacheUtils.DEFAULT_BUFFER_SIZE
                    int readBytes;
                    while ((readBytes = source.read(buffer)) != -1) {
                          synchronized (stopLock) {
                                if (isStopped()) {
                                    return;
                              }
                              cache.append(buffer, readBytes);  //追加到cache
                        }
                        offset += readBytes;
                        notifyNewCacheDataAvailable(offset, sourceAvailable);  //通知有数据可用,也就是唤醒waitForSourceData()方法,让while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped)继续判断执行下去
                  }
                    tryComplete();  //这个方法判断文件是否已经下载完成了。cache的文件是一个临时的.download为后缀的文件,分段缓存完成整个视频文件后,修改文件名为与请求url关联的文件名
                    onSourceRead();
              } catch (Throwable e) {
                    readSourceErrorsCount.incrementAndGet();
                    onError(e);
              } finally {
                    closeSource();
                    notifyNewCacheDataAvailable(offset, sourceAvailable);
              }
        }      

  7)responseWithoutCache(out, offset);方法的实现

   

private void responseWithoutCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
              HttpUrlSource newSourceNoCache = new HttpUrlSource(this.source);
              try {
                    newSourceNoCache.open((int) offset);  //这个方法里面打开HttpURLConnection
                    byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
                    int readBytes;
                    while ((readBytes = newSourceNoCache.read(buffer)) != -1) {  //read读取HttpURLConnection的getInputStream()的数据
                        out.write(buffer, 0, readBytes);
                        offset += readBytes;
                  }
                  out.flush();
            } finally {
                  newSourceNoCache.close();
            }
      }