1. 程式人生 > >AndroidVideoCache簡單使用及原始碼分析

AndroidVideoCache簡單使用及原始碼分析

        對於視訊播放,如果需要用到快取,AndroidVideoCach是一個不錯的選擇,該專案地址:

優缺點:

        優點:1、使用簡單,支援設定快取視訊的大小或個數;

                  2、支援斷點快取(一段視訊快取一部分後,退出關閉視訊後,下次再看時會從上次快取到的部分開始繼續快取);

        缺點:1、當快進視訊時,如果視訊沒快取到該位置,需要等視訊快取到這個點才會播放,不會直接跳到快進點開始快取;

使用:

        使用還是很簡單的,首先在Application中進行初始化:

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


private HttpProxyCacheServer newProxy() {
    return new HttpProxyCacheServer.Builder(this).cacheDirectory(new File(FileUtils.getCachedDirs(this)))
            //最大快取200M
            .maxCacheSize(200 * 1024 * 1024)
            .build();
}

這裡其實就是為了建立一個HttpProxyCacheServer的單例物件。你也可以根據自己的需求來建立。建立完這個物件後,接下來就是使用這個物件生成一個代理的url路徑了:

String proxyUrl = getProxy.getProxyUrl("urlPath");

urlPath指的是網路上的視訊路徑,返回的proxyUrl是一個代理路徑,得到這個代理路徑後,接下來就只需要將這個路徑設定給播放器就完成了。

        簡單的使用說完了,接下來就去看看HttpProxyCacheServer是如何構建的,從它的構建方法中我們才能知道他可以設定哪些引數以及有什麼作用:

public static final class Builder {

    private static final long DEFAULT_MAX_SIZE = 512 * 1024 * 1024;

    private File cacheRoot;
    private FileNameGenerator fileNameGenerator;
    private DiskUsage diskUsage;
    private SourceInfoStorage sourceInfoStorage;

    public Builder(Context context) {
        this.sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(context);
        this.cacheRoot = StorageUtils.getIndividualCacheDirectory(context);
        this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE);
        this.fileNameGenerator = new Md5FileNameGenerator();
    }

    //視訊檔案的快取路徑
    public Builder cacheDirectory(File file) {
        this.cacheRoot = checkNotNull(file);
        return this;
    }

    //生成的快取視訊檔案的名字,傳進來的物件實現FileNameGenerator這個類就可以了
    public Builder fileNameGenerator(FileNameGenerator fileNameGenerator) {
        this.fileNameGenerator = checkNotNull(fileNameGenerator);
        return this;
    }
    
    //設定快取檔案的大小,單位是bytes,預設是512M
    public Builder maxCacheSize(long maxSize) {
        this.diskUsage = new TotalSizeLruDiskUsage(maxSize);
        return this;
    }

    //設定快取檔案的個數,快取的策略只能是大小和個數中的一個
    public Builder maxCacheFilesCount(int count) {
        this.diskUsage = new TotalCountLruDiskUsage(count);
        return this;
    }

    //快取策略也可以自己定義,實現DiskUsage就可以了,看自己需要
    public Builder diskUsage(DiskUsage diskUsage) {
        this.diskUsage = checkNotNull(diskUsage);
        return this;
    }
    
    public HttpProxyCacheServer build() {
        Config config = buildConfig();
        return new HttpProxyCacheServer(config);
    }

    private Config buildConfig() {
        return new Config(cacheRoot, fileNameGenerator, diskUsage, sourceInfoStorage);
    }
}

從上面可以看出,構建HttpProxyCacheServer提供給我們可以設定的可以分為三類:

        1、設定快取檔案得名字,預設使用的是url的MD5碼;

        2、設定快取檔案的路徑,預設在路徑:Android/data/包名/cache;

        3、設定快取檔案的大小,預設是512M;

到這裡,使用就不成問題了,那如果你想知道它是如何實現的,那就繼續往下看。

原始碼分析:

        在看程式碼前,先說下它的整個設計思路,大體可以理解成兩部分,這也是我看完後自己的理解:

        1、開啟一個執行緒池去給定的路徑下下載檔案,將下載的檔案儲存到本地;

        2、視訊播放的時候讀儲存到本地的檔案,如果播放的地方還沒儲存到本地,那就需要等待視訊下載到這個地方才能播放;

        上面這種情況是邊播邊快取,如果在已經快取好去播視訊時,這時執行的邏輯就是直接播放本地視訊了;

        接下來就看看具體是如何實現的,首先要看的就是HttpProxyCacheServer的構造方法了:

private HttpProxyCacheServer(Config config) {
    this.config = checkNotNull(config);
    try {
        //使用Socket將本地的一個埠作為伺服器,這個埠號自動生成
        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);
    }
}

上面建立了一個執行緒,然後就開啟了,要看就應該是這個執行緒到底做了什麼,跟著程式碼走下去,最後執行的方法是waitForRequest():

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

程式碼還是挺少的,就是一個while迴圈,這個迴圈的條件就是當前執行緒沒有被中斷,這裡還有一個需要注意的地方就是ServerSocket的accept()方法,這是一個阻塞方法,對Socket不太瞭解的可以先去了解下,當有訪問這個ServerSocket的埠時,這是就會返回一個Socket物件,通過這個物件就可以與客戶端進行通訊了,這個Socket可以理解為就是視訊播放器那邊傳過來的,我們把視訊資料從這個Socket中返回,那視訊就可以播放了。socketProcess是一個執行緒池,把這個socket物件放進了執行緒中,線上程中有做了些什麼呢?那就看下SocketProcessRunnable這個物件的run()方法執行了什麼,它裡面呼叫的是processSocket()方法,那就看下這個方法:

private void processSocket(Socket socket) {
    try {
        //這裡就是獲取socket流中請求頭的資訊,然後建立了一個GetRequest物件並返回
        GetRequest request = GetRequest.read(socket.getInputStream());
        LOG.debug("Request to cache proxy:" + request);
        //request.uri是我們傳給播放器的代理url,這裡獲取的是真正的url
        String url = ProxyCacheUtils.decode(request.uri);
        //這個url是分為兩種情況的,一種是ping的時候傳的url,另一種就是真正請
        // 求資源的url路徑了,獲取代理路徑的時候就會去ping,感興趣的可以從獲
        // 取代理路徑那裡跟下去看
        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());
    }
}

獲取遠端資源是的邏輯是在HttpProxyCacheServerClients這個類中,這裡呼叫的是processRequest(),看來這裡可以算作是一個請求資源的入口了:

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);
    //遠端資源快取在本地的檔案
    FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage);
    HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache);
    httpProxyCache.registerCacheListener(uiCacheListener);
    return httpProxyCache;
}

接下來就是去看看HttpProxyCache的processRequest()方法了:

public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException {
    //拿到socket的輸出流
    OutputStream out = new BufferedOutputStream(socket.getOutputStream());
    //組裝響應頭資訊
    String responseHeaders = newResponseHeaders(request);
    //將組裝的頭資訊輸出到socket的輸出流中
    out.write(responseHeaders.getBytes("UTF-8"));

    long offset = request.rangeOffset;
    //是否使用快取,這裡分析使用快取的情況
    if (isUseCache(request)) {
        responseWithCache(out, offset);
    } else {
        responseWithoutCache(out, offset);
    }
}

繼續往下就是responseWithCache()方法了:

private void responseWithCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
    byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
    int readBytes;
    //read()方法是重點,這個方法是讀取資料,知道讀取資料完畢才會停止
    while ((readBytes = read(buffer, offset, buffer.length)) != -1) {
        //out是socket的輸出流,這裡就是將讀取的資料輸出給視訊播放
        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);
    //cache就是我們前面說的快取在本地的檔案
    //cache.isCompleted()本地是否已經全部快取完,沒有返回false
    //cache.available() < (offset + length)這個條件成立是:當前快取檔案的長度還沒快取到需要讀取資料的長度
    //stoped標記的是當前是否已經停止了
    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;
}

這裡就是主要的邏輯所在,前半部分是從伺服器讀取資料快取在本地,後半部分就是從本地快取區中讀取資料了,這裡我們先看簡單的,那就是cache的read()方法,這裡的cache是FileCache物件:

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

還是很簡單的,這裡的dataFile是一個RandomAccessFile物件,就是從這個檔案物件中去獲取資源,然後將獲取到的資源輸出到socket的輸出流中,也就是視訊播放了。

        接下來就是看去網路上獲取資源了,這裡看到方法是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();
    }
}

這裡又是開啟了一個執行緒,那自然還是看這個執行緒中的run()了,run()方法中執行的是readSource()方法:

private void readSource() {
    long sourceAvailable = -1;
    long offset = 0;
    try {
        //這個cache是FileCache物件,返回的就是已快取檔案的長度,
        // 這也從側面說明是支援斷點續傳的
        offset = cache.available();
        //這個source是一個HttpUrlSource物件,這裡面封裝的就
        // 是HttpUrlConnection對資源的獲取
        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);
    }
}

到這裡就全部都連線上了,通過HttpUrlConnection從伺服器進行資料的讀取,將讀取的資料快取在本地,然後播放的視訊資料就從快取中讀取的。今天就到這了,如果有什麼疑問,歡迎留言。