AndroidVideoCache源码赏析

前言

AndroidVideoCache 是用来帮助实现视频音频边缓存边播放的开源库,最近有个视频播放的功能,为了避免视频数据重复加载,就使用了这个库,感觉很棒,而且这个库的实现思路非常值得学习研究,就花点时间学习下它的源码。

原理

实现原理可以很简单的描述下:创建一个本地代理服务器,获取数据的请求不直接服务服务器而是访问代理服务器,代理服务器先判断请求是否有缓存,已经有缓存了的话就直接返回缓存数据,没有缓存的话代理服务器就服务资源服务器把资源缓存到本地再将缓存数据返回。

代码鉴赏

AndroidVideoCache使用很简单,创建一个代理服务器并返回一个代理地址,直接使用这个地址即可:

HttpProxyCacheServer proxy = AppApplication.getProxy(activity);
String proxyUrl = proxy.getProxyUrl(url);
videoView.setPath(proxyUrl);

private HttpProxyCacheServer proxy;
public static HttpProxyCacheServer getProxy(Context context) {
        return proxy;
    }
    private HttpProxyCacheServer newProxy() {
        return new HttpProxyCacheServer.Builder(this)
                .cacheDirectory(cacheDirFile)
                .maxCacheSize((1024+512)* 1024 * 1024 )
                .build();
    }

可以看到HttpProxyCacheServer使用的是建造者模式进行创建。既然是创建了一个代理服务器进行处理客户端发起的请求,那它是怎么创建代理服务器的,还有它是怎么监听并处理请求的?这个是我们必须要搞明白的。

首先我们先看看它是如何创建一个代理服务器的,注释很清楚了:

private HttpProxyCacheServer(Config config) {
       try {
           //获取本机的地址
           InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
           //创建一个ServerSocket去监听本地客户端发起的请求(使用localAddress参数来将ServerSocket绑定到PROXY_HOST,就是本机ip)
           this.serverSocket = new ServerSocket(0, 8, inetAddress);
           this.port = serverSocket.getLocalPort();
           //IgnoreHostProxySelector是继承ProxySelector的,它的作用是过滤一些需要忽略的请求
           //IgnoreHostProxySelector实际只用来ping
           IgnoreHostProxySelector.install(PROXY_HOST, port);
           CountDownLatch startSignal = new CountDownLatch(1);
          //WaitRequestsRunnable是一直循环等待请求进来
           this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
           this.waitConnectionThread.start();
           startSignal.await(); //freeze thread, wait for server starts
           //这个Pinger是用来ping处理请求的本地代理服务器判断是否可用,注意不是器远程
           //获取资源的代理服务器,是上面的PROXY_HOST serverSocket服务器
           this.pinger = new Pinger(PROXY_HOST, port);
           LOG.info("Proxy cache server started. Is it alive? " + isAlive());
       } catch (IOException | InterruptedException e) {
        ..
       }
   }

从上面,可以看得到:创建了ServerSocket也就是那个代理服务器来监听客户端的请求。 接下来我们看它是如何监听发起的请求还有是怎么处理请求的

从上面知道,WaitRequestsRunnable是用来监听请求的,它执行了一个方法,在循环等待请求进来:

private void waitForRequest() {
            while (!Thread.currentThread().isInterrupted()) {
                Socket socket = serverSocket.accept();
                socketProcessor.submit(new SocketProcessorRunnable(socket));
            }
    }
    private final class SocketProcessorRunnable implements Runnable {
            ...
            @Override
            public void run() {
                processSocket(socket);
            }
        }

        private void processSocket(Socket socket) {
        try {
            //客户端发来的数据其实就是请求数据,读取出来封装成GetRequest
            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 {
                //代理缓存服务器Client处理这个请求,clients会缓存起来
                //如果没有将创建一个HttpProxyCacheServerClients
                HttpProxyCacheServerClients clients = getClients(url);
                clients.processRequest(request, socket);
            }
        } catch (SocketException e) {
          ...
    }

可以看到了processSocket进行了处理请求的,但是看起来有点怪, GetRequest.read(socket.getInputStream())怎么读出来是一个请求?其实这也是很合理了,客户端发起的就是get请求来获取数据,所以socketServer服务器收到客户端发来的数据,其实就是请求数据,没有其他了,把这个数据读取出来,封装成GetRequest请求。

public static GetRequest read(InputStream inputStream) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
        StringBuilder stringRequest = new StringBuilder();
        String line;
        while (!TextUtils.isEmpty(line = reader.readLine())) { // until new line (headers ending)
            stringRequest.append(line).append('\n');
        }
        return new GetRequest(stringRequest.toString());
    }

把请求读取出来后最终使用了HttpProxyCacheServerClients的processRequest去处理这个请求。

public void processRequest(GetRequest request, Socket socket)  {
        //开始处理请求,startProcessRequest方法这里为了创建HttpProxyCache
        startProcessRequest();
        try {
            clientsCount.incrementAndGet();
            //使用HttpProxyCache处理请求
            proxyCache.processRequest(request, socket);
        } finally {
            finishProcessRequest();
        }
    }

    private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
      //可以看到,实质上还是通过HttpUrlSource去获取资源数据
        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;
    }

可以看到HttpProxyCache才是最终处理请求的类,它获取资源数据以及判断资源文件大小实质上还是通过HttpUrlSource。
HttpProxyCache的processRequest如下:

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;
        //判断是否需要使用缓存,
        //判断是否使用缓存满足条件:
        // 1.远程文件大小可以缓存
        // 2.不是特殊的请求(比如不是get请求)
        // 3.请求文件跳过的长度rangeOffset不能超过本地缓存文件的大小
        if (isUseCache(request)) {
          //如果是使用缓存的话,调用responseWithCache处理
            responseWithCache(out, offset);
        } else {
          //不使用缓存的话,直接使用HttpUrlSource读取文件并将读取回来的文件写出
            responseWithoutCache(out, offset);
        }
    }

    private void responseWithoutCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
       HttpUrlSource newSourceNoCache = new HttpUrlSource(this.source);
       try {
           newSourceNoCache.open((int) offset);
           byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
           int readBytes;
           while ((readBytes = newSourceNoCache.read(buffer)) != -1) {
               out.write(buffer, 0, readBytes);
               offset += readBytes;
           }
           out.flush();
       } finally {
           newSourceNoCache.close();
       }
   }

上面的流程其实到这里已经很清楚了,还有个问题是,加载回来的数据是怎么缓存起来的,是何如读取的呢?接下来我们看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();
   }

下面只要看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();
       }
       //获取数据并缓存完成后,将缓存数据发送出去
       //还记得创建HttpProxyCache时传入的那个FileCache吗,
       //这个cache就是那个FileCache,如果缓存完成后
       //percentsAvailable变为100
       int read = cache.read(buffer, offset, length);
       if (cache.isCompleted() && percentsAvailable != 100) {
           percentsAvailable = 100;
           onCachePercentsAvailableChanged(100);
       }
       return read;
   }

整个流程已经分析完成了,还是比较容易明白的,作者代码写的很好,很容易理解,下面总结下画个简单的
转载注明:https://mp.csdn.net/mdeditor/83277930#
流程图:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值