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中拿到數據並播放。

    


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM