Android VideoCache缓存框架分析

一、概述

  AndroidVideoCache是一个视频缓存框架,支持边下载边播放。

  基本原理:使用本地代理代替直接根据url请求网络服务。

      1.首先在本地新建一个服务(ServerSocket),监听客户端的接入,一旦有客户端接入就新建一个Socket来维持客户端和服务端之间的通讯。

      2.转换url,在客户端请求的时候使用proxy.getProxyUrl(url)把网络url转为本地url,调用这个url的时候会和本地的代理服务ServerSocket连接(也就是第一步)

      3.根据第二步,连接成功后创建一个HttpProxyCacheServerClients客户端,并执行request方法。

      4.创建HttpProxyCache类,并执行其processRequest方法回复数据。

      5.判断文件是否下载完成,如果没有下载完成,则单独开启一个线程进行异步下载(readSourceAsync)下载的数据会存入缓存。之后从缓存中读取数据(cache.read),也就是是播放器只读缓存数据

  

 

二、代码示例分析

  入口是HttpProxyCacheServer.java类。

  1.在Application中初始化HttpProxyCacheServer类,并建立本地代理服务

public static HttpProxyCacheServer getProxy(Context context) {
        App app = (App) context.getApplicationContext();
        return app.proxy == null ? (app.proxy = app.newProxy()) : app.proxy;
    }

    private HttpProxyCacheServer newProxy() {
        return new HttpProxyCacheServer.Builder(this)
                .cacheDirectory(Utils.getVideoCacheDir(this))
                .build();
    }

  在HttpProxyCacheServer的构造方法中会做一系列的初始化动作及新建一个代理服务,代码如下

private HttpProxyCacheServer(Config config) {
        this.config = checkNotNull(config);
        try {
            InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
            /**ServerSocket(port,backlog,inetAddress)参数说明:
             * port:第一个参数为端口号,如果填写0,则系统自动会分配一个端口号
             * backlog:第二个参数:最多同时可支持多少个链接
             * inetAddress:主机地址
             * */
            this.serverSocket = new ServerSocket(0, 8, inetAddress);
            //获取随机分配的端口号
            this.port = serverSocket.getLocalPort();
            IgnoreHostProxySelector.install(PROXY_HOST, port);
            /**信号量:其目的是为了让waitConnectionThread的run方法先执行*/
            CountDownLatch startSignal = new CountDownLatch(1);
            /**创建并开启一个一直等待客户端连接的线程*/
            this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
            this.waitConnectionThread.start();
            startSignal.await(); // freeze thread, wait for server starts
            //创建一个ping实例
            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);
        }
    }

  其中上述代码:startSignal信号量是用来确保WaitRequestRunnable类的run方法先执行,WaitRequestsRunnable线程使用来开启代理服务,并等待接受客户端的链接

 /**
     * 一直等待客户端连接的线程
     */
    private final class WaitRequestsRunnable implements Runnable {

        private final CountDownLatch startSignal;

        public WaitRequestsRunnable(CountDownLatch startSignal) {
            this.startSignal = startSignal;
        }

        @Override
        public void run() {
            startSignal.countDown();//确保run方法先执行
            waitForRequest();
        }
    }

  通过waitForRequest()等待客户端的接入,并将创建好的客户端Socket放入socketProcessor线程池中执行

 /**
     * 死循环通过serverSocket.accept()一直阻塞等待客户端连接
     */
    private void waitForRequest() {
        try {
            while (!Thread.currentThread().isInterrupted()) {
                //一直等待有客户端连接进来
                Socket socket = serverSocket.accept();
                LOG.info("Accept new socket " + socket);
                //连接进来之后立马提交给线程池执行
                socketProcessor.submit(new SocketProcessorRunnable(socket));
            }
        } catch (IOException e) {
            onError(new ProxyCacheException("Error during waiting connection", e));
        }
    }

  到这里初始化这块已经结束了。首先ServerSocket服务建立好了,其次接受客户端连接的线程也创建好了,最后接收客户端Socket后会放入线程池中执行。

 

2.接下来就是客户端请求服务端,代码如下:

private void startVideo() {
        HttpProxyCacheServer proxy = App.getProxy(getActivity());
        proxy.registerCacheListener(this, url);
        //将url转换为代理url
        String proxyUrl = proxy.getProxyUrl(url);
        Log.d(LOG_TAG, "Use proxy url " + proxyUrl + " instead of original url " + url);
        videoView.setVideoPath(proxyUrl);
        videoView.start();
    }

在上述代码中最主要的是proxy.getProxyUrl(url)方法,此方法是把网络url编码为本地的url,然后videoView.setVideoPath()设置本地请求路径,之后通过videoView.start()方法通过本地路径请求服务,此请求会到本地代理服务ServerSocket中。即会放到SocketProcessorRunnable类中执行。

 请求到这里算是结束了,接下来会看一下请求后的操作(这块是最主要的)

 

 3.客户端和服务端连通成功后的操作。其实这里会分成两块,一块是播放器和本地代理服务,另一块是本地代理服务和真正的云端服务。

  调用客户端HttpProxyCacheServerClients的processRequest方法创建一个HttpProxyCache类,利用该类的processRequest方法发起(本地)网络请求

 public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException {
        startProcessRequest();
        try {
            clientsCount.incrementAndGet();
            proxyCache.processRequest(request, socket);
        } finally {
            finishProcessRequest();
        }
    }

  封装响应头并给客户端回复消息

 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);
        }
    } 
  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方法。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;
    }

  此方法做了两件主要的事情:1.检测文件是否已经下载完成(readSourceAsync();),如果没有下载完成就接着下载,如果下载完成就跳过,代码如下:

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

  在SourceReaderRunnable中会调用run方法执行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];
            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);
        }
    }

  此方法的作用是接着上次没下载完的文件继续下载,下载后写入缓存文件,并实时通知客户端下载进度

  read方法做的第二件事情就是从缓存中读取数据给客户端

@Override
    public synchronized int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
        try {
            dataFile.seek(offset);
            return dataFile.read(buffer, 0, length);
        } catch (IOException e) {
            String format = "Error reading %d bytes with offset %d from file[%d bytes] to buffer[%d bytes]";
            throw new ProxyCacheException(String.format(format, length, offset, available(), buffer.length), e);
        }
    }

  

  总结:到此AndroidVideoCache的源代码已经分析完毕了。其最主要的流程便是:代理服务器连接真是的网络url并把文件下载到本地cache,客户端连接本地代理服务器,并从本地cache中拿到数据并播放。

    

posted on 2021-12-07 17:14  飘杨......  阅读(1623)  评论(0)    收藏  举报