AndroidVideoCache源码分析

HttpProxyCacheServer是AndroidVideoCache的对外入口,所以我们先来看一下它:

 private static final Logger LOG = LoggerFactory.getLogger("HttpProxyCacheServer");
    private static final String PROXY_HOST = "127.0.0.1";

    private final Object clientsLock = new Object();
    private final ExecutorService socketProcessor = Executors.newFixedThreadPool(8);
    private final Map clientsMap = new ConcurrentHashMap<>();
    private final ServerSocket serverSocket;
    private final int port;
    private final Thread waitConnectionThread;
    private final Config config;
    private final Pinger pinger;

    public HttpProxyCacheServer(Context context) {
        this(new Builder(context).buildConfig());
    }

    private HttpProxyCacheServer(Config config) {
        this.config = checkNotNull(config);
        try {
            InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
            this.serverSocket = new ServerSocket(0, 8, inetAddress);
            this.port = serverSocket.getLocalPort();
            IgnoreHostProxySelector.install(PROXY_HOST, port);
            CountDownLatch startSignal = new CountDownLatch(1);
            this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
            this.waitConnectionThread.start();
            startSignal.await(); // freeze thread, wait for server starts
            this.pinger = new Pinger(PROXY_HOST, port);
            LOG.info("Proxy cache server started. Is it alive? " + isAlive());
        } catch (IOException | InterruptedException e) {
            socketProcessor.shutdown();
            throw new IllegalStateException("Error starting local proxy server", e);
        }
    }
...
  1. 构造一个本地127.0.0.1的ServerSocker,随机分配端口
  2. 启动一个线程去执行WaitRequestsRunnable,在这里面执行waitForRequest,通过accept()来监听这个服务器soket的连接,accept()会一直阻塞,直到一个客户端连接上来。
     private void waitForRequest() {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    Socket socket = serverSocket.accept();
                    LOG.debug("Accept new socket " + socket);
                    socketProcessor.submit(new SocketProcessorRunnable(socket));
                }
            } catch (IOException e) {
                onError(new ProxyCacheException("Error during waiting connection", e));
            }
        }
  1. 有个信号量用来保证Server启动后再走往下的流程,这里构造一个Pinger,用来查看服务是否可用。
    CountDownLatch startSignal = new CountDownLatch(1);
    this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
    this.waitConnectionThread.start();
    startSignal.await(); // freeze thread, wait for server starts
    this.pinger = new Pinger(PROXY_HOST, port);
  1. HttpProxyCacheServer已经启动好了,在等待客户端连接。
  2. getProxyUrl使客户端连接到Server
    public String getProxyUrl(String url, boolean allowCachedFileUri) {
            if (allowCachedFileUri && isCached(url)) {
                File cacheFile = getCacheFile(url);
                touchFileSafely(cacheFile);
                return Uri.fromFile(cacheFile).toString();
            }
            return isAlive() ? appendToProxyUrl(url) : url;
        }

如果本地有缓存,那么会返回本地地址的Uri,file://uri,并且touch一下文件,把时间更新到最新,这里采用LruCache是根据文件被访问的时间进行排序的;否则先走一下isAlive()方法,这里会ping一下Server,确保是通的,如果不通的话,就直接返回原url,通的话返回代理url。

    private static final String PROXY_HOST = "127.0.0.1";
        private String appendToProxyUrl(String url) {
        return String.format(Locale.US, "http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url));
        }

所以视频播放器拿着代理url发起请求会和Server进行连接,这时候waitForRequest就会返回一个客户端的socket,用于Server和客户端通信,然后用线程池处理这个请求,可以看到最多支持8个并发连接。

    private final ExecutorService socketProcessor = Executors.newFixedThreadPool(8);
  1. SocketProcessorRunnable请求会通过processSocket进行处理,ping的过程其实也会被这个socket监听并且走进下面方法内,资源请求会走到else逻辑中。
       private void processSocket(Socket socket) {
               try {
                   GetRequest request = GetRequest.read(socket.getInputStream());
                   LOG.debug("Request to cache proxy:" + request);
                   String url = ProxyCacheUtils.decode(request.uri);
                   if (pinger.isPingRequest(url)) {
                       pinger.responseToPing(socket);
                   } else {
                       HttpProxyCacheServerClients clients = getClients(url);
                       clients.processRequest(request, socket);
                   }
               } catch (SocketException e) {
                   // There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458
                   // So just to prevent log flooding don't log stacktrace
                   LOG.debug("Closing socket… Socket is closed by client.");
               } catch (ProxyCacheException | IOException e) {
                   onError(new ProxyCacheException("Error processing request", e));
               } finally {
                   releaseSocket(socket);
                   LOG.debug("Opened connections: " + getClientsCount());
               }
           }
  1. 看在内存缓存,其实就是ConcurrentHashMap,看看有没有url对应的HttpProxyCacheServerClients,没有的话构造一个。HttpProxyCacheServerClients就是用来处理一个请求url对应的工作,如下所示:
       public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException {
               startProcessRequest();
               try {
                   clientsCount.incrementAndGet();
                   proxyCache.processRequest(request, socket);
               } finally {
                   finishProcessRequest();
               }
           }

通过startProcessRequest()构造HttpProxyCache

       private synchronized void startProcessRequest() throws ProxyCacheException {
               proxyCache = proxyCache == null ? newHttpProxyCache() : proxyCache;
           }
       
           private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
               HttpUrlSource source = new HttpUrlSource(url, config.sourceInfoStorage, config.headerInjector);
               FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage);
               HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache);
               httpProxyCache.registerCacheListener(uiCacheListener);
               return httpProxyCache;
           }

从上面可以分析出来,不过网络请求HttpUrlSource还是缓存FileCache都是通过HttpProxyCache来管理的,然后注册一个回调CacheListener,在HttpProxyCache缓存可用的时候会回调通知HttpProxyCacheServerClients,Clients就可以通知监听者:

       httpProxyCache.registerCacheListener(uiCacheListener);
       this.uiCacheListener = new UiListenerHandler(url, listeners);
       
           private static final class UiListenerHandler extends Handler implements CacheListener {
       
               private final String url;
               private final List listeners;
       
               public UiListenerHandler(String url, List listeners) {
                   super(Looper.getMainLooper());
                   this.url = url;
                   this.listeners = listeners;
               }
       
               @Override
               public void onCacheAvailable(File file, String url, int percentsAvailable) {
                   Message message = obtainMessage();
                   message.arg1 = percentsAvailable;
                   message.obj = file;
                   sendMessage(message);
               }
       
               @Override
               public void handleMessage(Message msg) {
                   for (CacheListener cacheListener : listeners) {
                       cacheListener.onCacheAvailable((File) msg.obj, url, msg.arg1);
                   }
               }
           }
  1. 看HttpProxyCacheServerClients构造函数,接下来会调用proxyCache.processRequest(request, socket):
       public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException {
               OutputStream out = new BufferedOutputStream(socket.getOutputStream());
               String responseHeaders = newResponseHeaders(request);
               out.write(responseHeaders.getBytes("UTF-8"));
       
               long offset = request.rangeOffset;
               if (isUseCache(request)) {
                   responseWithCache(out, offset);
               } else {
                   responseWithoutCache(out, offset);
               }
           }
  1. 首先通过Socket回消息给视频播放器头部信息,接下来判断是否需要走缓存,还是通过HttpUrlSource发起HttpURLConnection,读取数据并通过Socket返回给播放器。如果走缓存,则会调用read读取8k的数据,读取成功后通过Socket返回给播放器。再重复读直到完成。
       static final int DEFAULT_BUFFER_SIZE = 8 * 1024;
       
           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();
           }

read方法:

        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;
           }

10.通过循环不断读取数据,直到以下条件其中一个满足:

  • 文件读取完成
  • 或者读取的数据已经达到length的要求,默认是8k
  • Clients已经调用shutdown
  1. 读取数据会启动一个新的线程去读取:
      private synchronized void readSourceAsync() throws ProxyCacheException {
            boolean readingInProgress = sourceReaderThread != null && sourceReaderThread.getState() != Thread.State.TERMINATED;
            if (!stopped && !cache.isCompleted() && !readingInProgress) {
                sourceReaderThread = new Thread(new SourceReaderRunnable(), "Source reader for " + source);
                sourceReaderThread.start();
            }
        }
  1. 在SourceReaderRunnable中主要就是调用readSource,这里主要是通过HttpUrlSource.read读取网络数据,然后通过FileCache写入到本地缓存,在缓存结束后同样也会发送一个通知通知自己已经缓存完了,回调由外界控制
    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];
                int readBytes;
                while ((readBytes = source.read(buffer)) != -1) {
                    synchronized (stopLock) {
                        if (isStopped()) {
                            return;
                        }
                        cache.append(buffer, readBytes);
                    }
                    offset += readBytes;
                    notifyNewCacheDataAvailable(offset, sourceAvailable);
                }
                tryComplete();
                onSourceRead();
            } catch (Throwable e) {
                readSourceErrorsCount.incrementAndGet();
                onError(e);
            } finally {
                closeSource();
                notifyNewCacheDataAvailable(offset, sourceAvailable);
            }
        }

当readSourceAsync启动另外一个线程(为了方便这里简称为ThreadB)后,本线程(为了方便这里简称为ThreadA)会接下来执行 waitForSourceData, 先获得wc这个锁,然后调用ThreadA会挂起1s的时间或者ThreadB已经写完缓存,通过notifyAll通知。

     private void waitForSourceData() throws ProxyCacheException {
            synchronized (wc) {
                try {
                    wc.wait(1000);
                } catch (InterruptedException e) {
                    throw new ProxyCacheException("Waiting source data is interrupted!", e);
                }
            }
        }
       private void notifyNewCacheDataAvailable(long cacheAvailable, long sourceAvailable) {
           onCacheAvailable(cacheAvailable, sourceAvailable);
   
           synchronized (wc) {
               wc.notifyAll();
           }
       }

接下来ThreadA会继续执行checkReadSourceErrorsCount方法,如果ThreadB在readSource出现异常,会增加一次错误次数,然后会抛出异常。

  private static final int MAX_READ_SOURCE_ATTEMPTS = 1;
  
      private void checkReadSourceErrorsCount() throws ProxyCacheException {
          int errorsCount = readSourceErrorsCount.get();
          if (errorsCount >= MAX_READ_SOURCE_ATTEMPTS) {
              readSourceErrorsCount.set(0);
              throw new ProxyCacheException("Error reading source " + errorsCount + " times");
          }
      }

线程ThreadA会在while循环中继续判断条件,如果满足会跳出,然后从FileCache中读取length字节的数据返回到HttpProxyCache的responseWithCache方法中,通过Socket写回给播放器。

到此整个读取数据,缓存数据的流程就结束了。

总结

简单说起来就是通过代理策略,拦截网络请求,从本地拿出数据给到播放器。

你可能感兴趣的:(AndroidVideoCache源码分析)