android視頻緩存框架 [AndroidVideoCache](https://github.com/danikula/AndroidVideoCache) 源碼解析與評估

android視頻緩存框架 AndroidVideoCache 源碼解析與評估

引言

android中許多視頻播放框架都會有切換清晰度的選項, 而最佳的播放清晰度和流暢度無非是本地播放視頻了; AndroidVideoCache 允許添加緩存支持 VideoView/MediaPlayer,ExoPlayer,或其他單行播放器;

基本原理爲: 通過在本地構建一個服務器,再使用socket連接,通過socket讀取流數據;

特徵:

  • 在加載流時緩存至本地中;
  • 緩存資源離線工作;
  • 部分加載;
  • 自定義緩存限制;
  • 同一個url多客戶端支持;

該項目僅支持 直接url 媒體文件,並不支持如 DASH, SmoothStreaming, HLS等流媒體;

本次 代碼解析版本爲 com.danikula:videocache:2.7.1

使用方式

其中的一個使用方式

然後通過 String proxyUrl = ApplicationDemo.getProxy(mContext).getProxyUrl(VIDEO_URL); 獲取代理後url用於視頻播放;

關鍵類解析

HttpProxyCacheServer 代理緩存服務類

提供配置構造者,系統入口及功能整合;


	private static final Logger LOG = LoggerFactory.getLogger("HttpProxyCacheServer");
	//本地ip地址,用於構建本地socket;
    private static final String PROXY_HOST = "127.0.0.1";

	//client 的鎖對象;
    private final Object clientsLock = new Object();
	//固定線程數線程池;
    private final ExecutorService socketProcessor = Executors.newFixedThreadPool(8);
	//client 的 線程安全容器,key 爲 url;
    private final Map<String, HttpProxyCacheServerClients> clientsMap = new ConcurrentHashMap<>();
	//服務端socket,用於阻塞等待socket連入;
    private final ServerSocket serverSocket;
	//端口
    private final int port;
	//等待socket連接子線程;
    private final Thread waitConnectionThread;
	//server 構建配置;
    private final Config config;
	//ping 系統,用於判斷是否連接;
    private final Pinger pinger;
	
	//>>>>>>>> 這裏是初始化的入口: 
	public HttpProxyCacheServer(Context context) {
		//使用默認的配置構建server;
        this(new Builder(context).buildConfig());
    }

    private HttpProxyCacheServer(Config config) {
        this.config = checkNotNull(config);
        try {
            InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
			//todo 使用本地ip地址建立服務端socket;
            this.serverSocket = new ServerSocket(0, 8, inetAddress);
			//服務端端口;
            this.port = serverSocket.getLocalPort();
			//ProxySelector 關鍵類:爲當前的socket的host和端口忽略默認代理;
            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
			//Pinger 關鍵類:
            this.pinger = new Pinger(PROXY_HOST, port);
			//使用pinger 去判斷ServerSocket是否存活;
            LOG.info("Proxy cache server started. Is it alive? " + isAlive());
        } catch (IOException | InterruptedException e) {
			//中斷時直接shutdown線程池;
            socketProcessor.shutdown();
            throw new IllegalStateException("Error starting local proxy server", e);
        }
    }
	
	//子線程運行
	private final class WaitRequestsRunnable implements Runnable {

        private final CountDownLatch startSignal;

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

		//線程運行時,countDownLatch打開,死循環等待外部socket接入;
        @Override
        public void run() {
			//notify freezed thread;
            startSignal.countDown();
            waitForRequest();
        }
    }


	private void waitForRequest() {
        try {
			//中斷時結束循環
            while (!Thread.currentThread().isInterrupted()) {
				//阻塞當前子線程(waitConnectionThread)
                Socket socket = serverSocket.accept();
                LOG.debug("Accept new socket " + socket);
				//已接入一個外部socket,線程池運行runnable,調用`processSocket(socket);`
                socketProcessor.submit(new SocketProcessorRunnable(socket));
            }
        } catch (IOException e) {
            onError(new ProxyCacheException("Error during waiting connection", e));
        }
    }

	//線程池運行
	private void processSocket(Socket socket) {
        try {
			//讀取socket中輸入流; 記錄range 和 url 等請求數據;
            GetRequest request = GetRequest.read(socket.getInputStream());
            LOG.debug("Request to cache proxy:" + request);
			//url Decode, 此url 爲 URL中定位的資源,ping或者videoUrl;
            String url = ProxyCacheUtils.decode(request.uri);
			
			//如果輸入流中url 爲`ping`,則返回連接狀態ok;
            if (pinger.isPingRequest(url)) {
                pinger.responseToPing(socket);
            } else {
				//建立client,響應請求;
                HttpProxyCacheServerClients clients = getClients(url);
				//使用與url綁定的client處理socket輸入流; 此處獲取真實加載的videoUrl處理;
                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 對象;
	private HttpProxyCacheServerClients getClients(String url) throws ProxyCacheException {
        synchronized (clientsLock) {
            HttpProxyCacheServerClients clients = clientsMap.get(url);
            if (clients == null) {
                clients = new HttpProxyCacheServerClients(url, config);
                clientsMap.put(url, clients);
            }
            return clients;
        }
    }

	// >>>>>>>> 2.代理videoUrl的方法入口;
	public String getProxyUrl(String url) {
        return getProxyUrl(url, true);
    }

	public String getProxyUrl(String url, boolean allowCachedFileUri) {
		//isCached 使用url 和命名生成器 判斷本地是否存在緩存文件;
        if (allowCachedFileUri && isCached(url)) {
            File cacheFile = getCacheFile(url);
			//如果存在,嘗試用diskUsage 的lru算法保存文件;
            touchFileSafely(cacheFile);
			//此處意爲,如果已經下載完成後,直接用本地緩存文件路徑播放;
            return Uri.fromFile(cacheFile).toString();
        }
		//如果serverSocket存活狀態, 拼接代理VideoUrl; 加載時觸發 `processSocket `方法
        return isAlive() ? appendToProxyUrl(url) : url;
    }

	//使用ping-ping ok 系統判斷本地ip是否能成功連通;
	private boolean isAlive() {
		//最大嘗試數3次,每次重新嘗試會翻倍timeout時間;
        return pinger.ping(3, 70);   // 70+140+280=max~500ms
    }

	//>>>>>>>>> 2. 核心處理videourl,使用本地代理ip; 請求時,獲取GET 的包頭信息 即videoUrl或者ping;
	private String appendToProxyUrl(String url) {
        return String.format(Locale.US, "http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url));
    }


**java.net.ProxySelector ** 代理選擇

{@link ProxySelector} that ignore system default proxies for concrete host.
ProxySelector 用於爲具體的host忽略系統默認的代理;

IgnoreHostProxySelector extends ProxySelector 修改系統默認proxySelector 忽略本地ip;


	//ProxySelector.java 靜態代碼塊中會進行初始化
	public abstract class ProxySelector {
		...
		static {
	        try {
	            Class var0 = Class.forName("sun.net.spi.DefaultProxySelector");
	            if (var0 != null && ProxySelector.class.isAssignableFrom(var0)) {
	                theProxySelector = (ProxySelector)var0.newInstance();
	            }
	        } catch (Exception var1) {
	            theProxySelector = null;
	        }
	
	    }

		public static ProxySelector getDefault() {
	        SecurityManager var0 = System.getSecurityManager();
	        if (var0 != null) {
	            var0.checkPermission(SecurityConstants.GET_PROXYSELECTOR_PERMISSION);
	        }
	
	        return theProxySelector;
	    }	
	
		public static void setDefault(ProxySelector var0) {
	        SecurityManager var1 = System.getSecurityManager();
	        if (var1 != null) {
	            var1.checkPermission(SecurityConstants.SET_PROXYSELECTOR_PERMISSION);
	        }
	
	        theProxySelector = var0;
	    }	
	}
	

	class IgnoreHostProxySelector extends ProxySelector {

	    private static final List<Proxy> NO_PROXY_LIST = Arrays.asList(Proxy.NO_PROXY);
	
	    private final ProxySelector defaultProxySelector;
	    private final String hostToIgnore;
	    private final int portToIgnore;
	
	    IgnoreHostProxySelector(ProxySelector defaultProxySelector, String hostToIgnore, int portToIgnore) {
	        this.defaultProxySelector = checkNotNull(defaultProxySelector);
	        this.hostToIgnore = checkNotNull(hostToIgnore);
	        this.portToIgnore = portToIgnore;
	    }
	
	    static void install(String hostToIgnore, int portToIgnore) {
			//獲取已經在靜態代碼塊中初始化的`theProxySelector:ProxySelector`
	        ProxySelector defaultProxySelector = ProxySelector.getDefault();
			//使用本地服務端socket的host和端口 建立ProxySelector的代理類;
	        ProxySelector ignoreHostProxySelector = new IgnoreHostProxySelector(defaultProxySelector, hostToIgnore, portToIgnore);
			//設置系統默認的代理ProxySelector對象;
	        ProxySelector.setDefault(ignoreHostProxySelector);
	    }
	
	    @Override
	    public List<Proxy> select(URI uri) {
	        boolean ignored = hostToIgnore.equals(uri.getHost()) && portToIgnore == uri.getPort();
			//如果是serverSocket的host和port則直接忽略,否則交給默認處理
	        return ignored ? NO_PROXY_LIST : defaultProxySelector.select(uri);
	    }
	
	    @Override
	    public void connectFailed(URI uri, SocketAddress address, IOException failure) {
	        defaultProxySelector.connectFailed(uri, address, failure);
	    }
	}

Pinger 判斷本地serverSocket是否存活

Pings {@link HttpProxyCacheServer} to make sure it works. 類似ping-pong 系統;如果請求是ping,則返回ping ok表示連接成功;


	//測試連接使用,判斷是否正確;
	private static final String PING_REQUEST = "ping";
    private static final String PING_RESPONSE = "ping ok";

    private final ExecutorService pingExecutor = Executors.newSingleThreadExecutor();
	//記錄serverSocket的host和port;
    private final String host;
    private final int port;

	//輸入流爲ping時的響應,向socket輸出 ping ok 表示連接成功;
	void responseToPing(Socket socket) throws IOException {
        OutputStream out = socket.getOutputStream();
        out.write("HTTP/1.1 200 OK\n\n".getBytes());
        out.write(PING_RESPONSE.getBytes());
    }

	//檢查serversocket 是否存活; ping- ping ok
	boolean ping(int maxAttempts, int startTimeout) {
        checkArgument(maxAttempts >= 1);
        checkArgument(startTimeout > 0);

        int timeout = startTimeout;
        int attempts = 0;
        while (attempts < maxAttempts) {
            try {
				//多線程設計 - 憑據設計; 開啓異步執行`pingServer()`方法,保存結果;
                Future<Boolean> pingFuture = pingExecutor.submit(new PingCallable());
				//執行callback call方法, 取得執行結果;
                boolean pinged = pingFuture.get(timeout, MILLISECONDS);
                if (pinged) {
                    return true;
                }
            } catch (TimeoutException e) {
                LOG.warn("Error pinging server (attempt: " + attempts + ", timeout: " + timeout + "). ");
            } catch (InterruptedException | ExecutionException e) {
                LOG.error("Error pinging server due to unexpected error", e);
            }
            attempts++;
            timeout *= 2;
        }
        String error = String.format(Locale.US, "Error pinging server (attempts: %d, max timeout: %d). " +
                        "If you see this message, please, report at https://github.com/danikula/AndroidVideoCache/issues/134. " +
                        "Default proxies are: %s"
                , attempts, timeout / 2, getDefaultProxies());
        LOG.error(error, new ProxyCacheException(error));
        return false;
    }

	private boolean pingServer() throws ProxyCacheException {
		//String.format(Locale.US, "http://%s:%d/%s", host, port, PING_REQUEST);
		//http://127.0.0.1:port/ping
        String pingUrl = getPingUrl();
        HttpUrlSource source = new HttpUrlSource(pingUrl);
        try {
            byte[] expectedResponse = PING_RESPONSE.getBytes();
			//通過HttpUrlConnection 請求本地ip建立的服務器 serverSocket; 並記錄信息;
            source.open(0);
            byte[] response = new byte[expectedResponse.length];
			//讀取固定的大小數據;
            source.read(response);
			//如果返回ping ok 表示存活狀態;
            boolean pingOk = Arrays.equals(expectedResponse, response);
            LOG.info("Ping response: `" + new String(response) + "`, pinged? " + pingOk);
            return pingOk;
        } catch (ProxyCacheException e) {
            LOG.error("Error reading ping response", e);
            return false;
        } finally {
            source.close();
        }
    }

GetRequest 封裝用於獲取請求中信息;

Model for Http GET request.

http get請求,處理socket輸入流,存儲range 和 url中定位的資源地址;
range 爲了在 HttpUrlConnection 中使用offset 獲取數據;

	//正則表達式匹配
	private static final Pattern RANGE_HEADER_PATTERN = Pattern.compile("[R,r]ange:[ ]?bytes=(\\d*)-");
    private static final Pattern URL_PATTERN = Pattern.compile("GET /(.*) HTTP");
	
	
    public final String uri;
    public final long rangeOffset;
    public final boolean partial;

	//read方法讀取socket的輸入流,拼接Stringbuilder,構建GetRequest對象;
	public GetRequest(String request) {
        checkNotNull(request);
		//使用 RANGE_HEADER_PATTERN 進行正則匹配, 並返回group(1), 即第一個`()`中的內容,即 bytes內容;
        long offset = findRangeOffset(request);
        this.rangeOffset = Math.max(0, offset);
        this.partial = offset >= 0;
		//使用 URL_PATTERN 進行正則匹配, 同樣返回group(1),即第一個()中的內容; 
		//獲取URL中定位的資源; 如真實的videoUrl或者ping;
        this.uri = findUri(request);
    }

HttpProxyCacheServerClients 以url爲key綁定的客戶端處理類

本地serverSocket服務器,接受到客戶端socket接入後, 對不同的url(請求行中URL字段;ping或videoUrl),使用容器單例對不同url新建client 處理請求;

其實大部分核心邏輯在HttpProxyCache中,此處提供回調和封裝;

	
	//原子int,防止併發;
	private final AtomicInteger clientsCount = new AtomicInteger(0);
    private final String url;
	//多線程;
    private volatile HttpProxyCache proxyCache;
	//緩存獲取 回調接口 , 這裏特地寫了一個 `發佈-訂閱` 模型 ,採用觀察者模式 擴展 回調;
    private final List<CacheListener> listeners = new CopyOnWriteArrayList<>();
	//這是一個集成`Handler`,實現CacheListener接口的類; 
	//作爲callback傳入`HttpProxyCache`內,收到回調後在回調給註冊的觀察者listeners;
    private final CacheListener uiCacheListener;
    private final Config config;

	//如果socket輸入不是ping請求,則new client 處理;
	public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException {
		//建立proxyCache;
        startProcessRequest();
        try {
			//只要socket連接進入,++,跟 clientsMap 不同;
            clientsCount.incrementAndGet();
			//proxyCache (HttpProxyCache 具體類) 處理請求;
            proxyCache.processRequest(request, socket);
        } finally {
            finishProcessRequest();
        }
    }
	
	//volatile ,多線程併發處理;
	private synchronized void startProcessRequest() throws ProxyCacheException {
        proxyCache = proxyCache == null ? newHttpProxyCache() : proxyCache;
    }

	
	private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
		//建立HttpUrlSource,使用配置類中自定義屬性;
        HttpUrlSource source = new HttpUrlSource(url, config.sourceInfoStorage, config.headerInjector);
		//添加緩存類,使用自定義url本地文件命名器和存儲管理類,默認爲(cacheRoot+md5後的url+url後綴)和lru算法;
        FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage);
        HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache);
        httpProxyCache.registerCacheListener(uiCacheListener);
        return httpProxyCache;
    }

	//繼承Handler,loop爲主線程輪詢器;
	private static final class UiListenerHandler extends Handler implements CacheListener {
        private final String url;
        private final List<CacheListener> listeners;

        public UiListenerHandler(String url, List<CacheListener> listeners) {
            super(Looper.getMainLooper());
            this.url = url;
            this.listeners = listeners;
        }

		//HttpProxyCache 回調後, 發送message,然後通知觀察者;
        @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);
            }
        }
    }

HttpProxyCache extend ProxyCache (數據處理核心類,響應數據構造及數據存儲控制;)

{@link ProxyCache} that read http url and writes data to {@link Socket}

使用 HttpUrlSource 和 FileCache 進行讀http url和寫回數據到socket中;


	//>>>>>>>>  此處運行完後,初始化就告一段落了;
	//處理接入的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);
        }
    }

	//響應頭的構建
	private String newResponseHeaders(GetRequest request) throws IOException, ProxyCacheException {
		//HttpUrlSource獲取mime,如果爲空則 調用 `fetchContentInfo` 獲取mime,或者length爲默認值時也會觸發此方法;
        String mime = source.getMime();
        boolean mimeKnown = !TextUtils.isEmpty(mime);
		//如果cache中已經完成了直接返回,否則length爲默認值調用 `fetchContentInfo`
        long length = cache.isCompleted() ? cache.available() : source.length();
        boolean lengthKnown = length >= 0;
		//range offset 不爲0,部分下載;
        long contentLength = request.partial ? length - request.rangeOffset : length;
        boolean addRange = lengthKnown && request.partial;
        return new StringBuilder()
                .append(request.partial ? "HTTP/1.1 206 PARTIAL CONTENT\n" : "HTTP/1.1 200 OK\n")
                .append("Accept-Ranges: bytes\n")
                .append(lengthKnown ? format("Content-Length: %d\n", contentLength) : "")
                .append(addRange ? format("Content-Range: bytes %d-%d/%d\n", request.rangeOffset, length - 1, length) : "")
                .append(mimeKnown ? format("Content-Type: %s\n", mime) : "")
                .append("\n") // headers end
                .toString();
    }

	//判斷是否使用本地緩存;
	private boolean isUseCache(GetRequest request) throws ProxyCacheException {
        long sourceLength = source.length();
        boolean sourceLengthKnown = sourceLength > 0;
        long cacheAvailable = cache.available();
        // do not use cache for partial requests which too far from available cache. It seems user seek video.
		// length未知 或者 不是部分請求 或者 部分請求偏移量 <= 已緩存量(RandomAccessFile)+ 固定offset 情況下使用緩存;
        return !sourceLengthKnown || !request.partial || request.rangeOffset <= cacheAvailable + sourceLength * NO_CACHE_BARRIER;
    }

	//使用本地緩存的情況下,寫出給Socket;
	private void responseWithCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];//8 * 1024
        int readBytes;
		//ProxyCache.read(), 每次取DEFAULT_BUFFER_SIZE的數據;
        while ((readBytes = read(buffer, offset, buffer.length)) != -1) {
            out.write(buffer, 0, readBytes);
            offset += readBytes;
        }
        out.flush();
    }

	//不使用本地緩存的情況
	private void responseWithoutCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
        HttpUrlSource newSourceNoCache = new HttpUrlSource(this.source);
        try {
			//打開inputStream,獲取source信息,用於後面的read數據;
            newSourceNoCache.open((int) offset);
            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
            int readBytes;
			//向socket的輸出流寫出數據; HttpUrlSource.read(); 即正常下載了;
            while ((readBytes = newSourceNoCache.read(buffer)) != -1) {
                out.write(buffer, 0, readBytes);
                offset += readBytes;
            }
            out.flush();
        } finally {
            newSourceNoCache.close();
        }
    }

------------------

	//ProxyCache.java 
	public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
        ProxyCacheUtils.assertBuffer(buffer, offset, length);
		
		//臨時文件沒有完成 cache的RandomAccessFile大小< offset + buffer大小  沒有被shutdown , 則一直在循環中;
        while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped) {
			//開啓一個線程異步下載數據到本地文件中,沒下載完一直處於死循環中;
            readSourceAsync();
			//此處是線程池中運行,等待1s;
            waitForSourceData();
			//檢查是否有錯誤;
            checkReadSourceErrorsCount();
        }
		//下載會把數據存入本地,讀取本地中的數據;
        int read = cache.read(buffer, offset, length);
		//使用緩存的數據,回調監聽;
        if (cache.isCompleted() && percentsAvailable != 100) {
            percentsAvailable = 100;
            onCachePercentsAvailableChanged(100);
        }
        return read;
    }

	//異步讀取資源;
	private synchronized void readSourceAsync() throws ProxyCacheException {
        boolean readingInProgress = sourceReaderThread != null && sourceReaderThread.getState() != Thread.State.TERMINATED;
        if (!stopped && !cache.isCompleted() && !readingInProgress) {
			//每次read讀取都會開啓一個線程 ,Runnable 做 `readSource` 操作;
            sourceReaderThread = new Thread(new SourceReaderRunnable(), "Source reader for " + source);
            sourceReaderThread.start();
        }
    }

	//while 循環中,下載完纔會退出;
	private void readSource() {
        long sourceAvailable = -1;
        long offset = 0;
        try {
			//RandomAccessFile 的 已獲得大小
            offset = cache.available();
			// HttpUrlSource open 斷點下載,open 打開inputStream,用於後面read;
            source.open(offset);
            sourceAvailable = source.length();
            byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
            int readBytes;
			//用byte[]不斷的讀取inputStream;
            while ((readBytes = source.read(buffer)) != -1) {
                synchronized (stopLock) {
                    if (isStopped()) {
                        return;
                    }
					//FileCache 的 RandomAccessFile 拼接數據, 通過RandomAccessFile寫到本地臨時文件中作爲緩存;
                    cache.append(buffer, readBytes);
                }
                offset += readBytes;
				//通知監聽;
                notifyNewCacheDataAvailable(offset, sourceAvailable);
            }
			//下載完畢,調用FileCache complete(),去除臨時文件名,更改file名稱;
            tryComplete();
			//percent 100,通知監聽;
            onSourceRead();
        } catch (Throwable e) {
            readSourceErrorsCount.incrementAndGet();
            onError(e);
        } finally {
            closeSource();
            notifyNewCacheDataAvailable(offset, sourceAvailable);
        }
    }

	private void notifyNewCacheDataAvailable(long cacheAvailable, long sourceAvailable) {
        onCacheAvailable(cacheAvailable, sourceAvailable);
		//notify ` waitForSourceData()`
        synchronized (wc) {
            wc.notifyAll();
        }
    }

	//獲取進度回調;
	protected void onCacheAvailable(long cacheAvailable, long sourceLength) {
        boolean zeroLengthSource = sourceLength == 0;
        int percents = zeroLengthSource ? 100 : (int) ((float) cacheAvailable / sourceLength * 100);
        boolean percentsChanged = percents != percentsAvailable;
        boolean sourceLengthKnown = sourceLength >= 0;
		//正在下載中的時候,回調數據到 UiListenerHandler(Handler),發送message,啓動觀察者;
        if (sourceLengthKnown && percentsChanged) {
            onCachePercentsAvailableChanged(percents);
        }
        percentsAvailable = percents;
    }

	//開啓下載異步線程後; 因爲這裏處於線程池中運行,這裏等待1s;
	private void waitForSourceData() throws ProxyCacheException {
        synchronized (wc) {
            try {
                wc.wait(1000);
            } catch (InterruptedException e) {
                throw new ProxyCacheException("Waiting source data is interrupted!", e);
            }
        }
    }

HttpUrlSource 數據網絡獲取 (HttpURLConnection實現)

{@link Source} that uses http resource as source for {@link ProxyCache}.

提供 open, length,read,close等接口方法; 使用Config中定義的屬性進行數據處理;


	//如果sourceInfo值爲默認值,則觸發此方法連接和更新SourceInfo; 建立連接,獲取數據信息;
	private void fetchContentInfo() throws ProxyCacheException {
        LOG.debug("Read content info from " + sourceInfo.url);
        HttpURLConnection urlConnection = null;
        InputStream inputStream = null;
        try {
            urlConnection = openConnection(0, 10000);
			//Content-Length
            long length = getContentLength(urlConnection);	
			//Content-Type
            String mime = urlConnection.getContentType();
            inputStream = urlConnection.getInputStream();
            this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
			//默認是存儲在本地數據庫;
            this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
            LOG.debug("Source info fetched: " + sourceInfo);
        } catch (IOException e) {
            LOG.error("Error fetching info from " + sourceInfo.url, e);
        } finally {
            ProxyCacheUtils.close(inputStream);
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
        }
    }

	//使用HttpURLConnection 進行網絡請求;
	private HttpURLConnection openConnection(long offset, int timeout) throws IOException, ProxyCacheException {
        HttpURLConnection connection;
        boolean redirected;
        int redirectCount = 0;
        String url = this.sourceInfo.url;
        do {
            LOG.debug("Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
            connection = (HttpURLConnection) new URL(url).openConnection();
			//使用配置項中自定義Header;
            injectCustomHeaders(connection, url);
			//GetRequest 中的 Range 參數;
            if (offset > 0) {
                connection.setRequestProperty("Range", "bytes=" + offset + "-");
            }
            if (timeout > 0) {
                connection.setConnectTimeout(timeout);
                connection.setReadTimeout(timeout);
            }
            int code = connection.getResponseCode();
			//301||302||303 重定向處理;
            redirected = code == HTTP_MOVED_PERM || code == HTTP_MOVED_TEMP || code == HTTP_SEE_OTHER;
            if (redirected) {
                url = connection.getHeaderField("Location");
                redirectCount++;
                connection.disconnect();
            }
			//默認 最大重定向5次;
            if (redirectCount > MAX_REDIRECTS) {
                throw new ProxyCacheException("Too many redirects: " + redirectCount);
            }
        } while (redirected);
        return connection;
    }

	
	//Opens source. Source should be open before using {@link #read(byte[])} 
	 @Override
    public void open(long offset) throws ProxyCacheException {
        try {
			//從offset 處,斷點下載;
            connection = openConnection(offset, -1);
            String mime = connection.getContentType();
			//建立連接,獲取流數據 inputStream;
            inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE);
			//如果是部分連接,則爲ContentLength+offset; 否則ContentLength;
            long length = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
			//更新數據;
            this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
			//存儲信息進本地;
            this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
        } catch (IOException e) {
            throw new ProxyCacheException("Error opening connection for " + sourceInfo.url + " with offset " + offset, e);
        }
    }

	//Read data to byte buffer from source with current offset.  每次默認大小的獲取流數據;
	 @Override
    public int read(byte[] buffer) throws ProxyCacheException {
        ...
        try {
			//流中讀取數據;
            return inputStream.read(buffer, 0, buffer.length);
        }...
    }

FileCache 數據緩存類 LRU

{@link Cache} that uses file for storing data.

提供 available,read,append,close,complete,isCompleted等接口方法;

	
	//臨時文件後綴;
	private static final String TEMP_POSTFIX = ".download";

    private final DiskUsage diskUsage;
	
    public File file;
    private RandomAccessFile dataFile;

	public FileCache(File file, DiskUsage diskUsage) throws ProxyCacheException {
        try {
            if (diskUsage == null) {
                throw new NullPointerException();
            }
            this.diskUsage = diskUsage;
			//file:  /cacheRoot/默認md5加密的文件名.後綴名; directory: /cacheRoot
            File directory = file.getParentFile();
			//建立目錄;
            Files.makeDir(directory);
            boolean completed = file.exists();
			//建立文件 /cacheRoot/默認md5加密文件名.後綴名+ 臨時文件後綴;
            this.file = completed ? file : new File(file.getParentFile(), file.getName() + TEMP_POSTFIX);
            this.dataFile = new RandomAccessFile(this.file, completed ? "r" : "rw");
        } catch (IOException e) {
            throw new ProxyCacheException("Error using file " + file + " as disc cache", e);
        }
    }

	@Override
    public synchronized void append(byte[] data, int length) throws ProxyCacheException {
        try {
            ...	
			//下載完數據,寫入本地File (RandomAccessFile)
            dataFile.seek(available());
            dataFile.write(data, 0, length);
        } ...
    }

	@Override
    public synchronized void complete() throws ProxyCacheException {
        if (isCompleted()) {
            return;
        }

        close();
		//去除臨時後綴名;
        String fileName = file.getName().substring(0, file.getName().length() - TEMP_POSTFIX.length());
        File completedFile = new File(file.getParentFile(), fileName);
		//重命名本地存儲文件;
        boolean renamed = file.renameTo(completedFile);
        if (!renamed) {
            throw new ProxyCacheException("Error renaming file " + file + " to " + completedFile + " for completion!");
        }
        file = completedFile;
        try {
            dataFile = new RandomAccessFile(file, "r");
			//默認Lru算法存儲文件;
            diskUsage.touch(file);
        } catch (IOException e) {
            throw new ProxyCacheException("Error opening " + file + " as disc cache", e);
        }
    }

	//讀取緩存中的數據;
	@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);
        }
    }


Config 配置類及構造者

構造者模式,方便設置自定義參數;

	
	class Config {

	    public final File cacheRoot; //自定義緩存目錄;
	    public final FileNameGenerator fileNameGenerator;//自定義文件名稱生成器;
	    public final DiskUsage diskUsage; //自定義緩存管理設置 (存儲至本地);
	    public final SourceInfoStorage sourceInfoStorage;//自定義數據信息存儲(url,length,mime等數據);
		public final HeaderInjector headerInjector; //自定義添加請求頭數據;
	
	    Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage, SourceInfoStorage sourceInfoStorage) {
	        this.cacheRoot = cacheRoot;
	        this.fileNameGenerator = fileNameGenerator;
	        this.diskUsage = diskUsage;
	        this.sourceInfoStorage = sourceInfoStorage;
	    }
	
	    File generateCacheFile(String url) {
	        String name = fileNameGenerator.generate(url);
	        return new File(cacheRoot, name);
	    }

	}

	//**默認參數獲取:**

	// 其中默認緩存路徑cacheRoot:由`StorageUtils.getIndividualCacheDirectory(context)`獲取;
	// 定義緩存目錄的方法如下:
	private static File getCacheDirectory(Context context, boolean preferExternal) {
        File appCacheDir = null;
        String externalStorageState;
        try {
            externalStorageState = Environment.getExternalStorageState();
        } catch (NullPointerException e) { // (sh)it happens
            externalStorageState = "";
        }
        if (preferExternal && MEDIA_MOUNTED.equals(externalStorageState)) {
			//sd卡存儲路徑/Android/data/[app_package_name]/cache/
            appCacheDir = getExternalCacheDir(context);
        }
        if (appCacheDir == null) {
			//手機 devices file system;
            appCacheDir = context.getCacheDir();
        }
        if (appCacheDir == null) {
			///data/data/[app_package_name]/cache/
            String cacheDirPath = "/data/data/" + context.getPackageName() + "/cache/";
            LOG.warn("Can't define system cache directory! '" + cacheDirPath + "%s' will be used.");
            appCacheDir = new File(cacheDirPath);
        }
		//然後在拼接 `/video-cache/` 路徑;
        return appCacheDir;
    }

    private static File getExternalCacheDir(Context context) {
        File dataDir = new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data");
        File appCacheDir = new File(new File(dataDir, context.getPackageName()), "cache");
        if (!appCacheDir.exists()) {
            if (!appCacheDir.mkdirs()) {
                LOG.warn("Unable to create external cache directory");
                return null;
            }
        }
        return appCacheDir;
    }

	//fileNameGenerator 本地存儲文件命名管理默認配置爲:
	public class Md5FileNameGenerator implements FileNameGenerator {

	    private static final int MAX_EXTENSION_LENGTH = 4;
	
		//md5加密的url + url後綴名
	    @Override
	    public String generate(String url) {
	        String extension = getExtension(url);
	        String name = ProxyCacheUtils.computeMD5(url);
	        return TextUtils.isEmpty(extension) ? name : name + "." + extension;
	    }

		//獲取url的後綴名 ,如mp4;
	    private String getExtension(String url) {
	        int dotIndex = url.lastIndexOf('.');
	        int slashIndex = url.lastIndexOf('/');
	        return dotIndex != -1 && dotIndex > slashIndex && dotIndex + 2 + MAX_EXTENSION_LENGTH > url.length() ?
	                url.substring(dotIndex + 1, url.length()) : "";
	    }
	}

	//diskUsage 存儲管理默認配置爲: TotalSizeLruDiskUsage(總大小限制,默認爲512M) ;
	// 繼承於`LruDiskUsage`,使用Lru算法;
	public abstract class LruDiskUsage implements DiskUsage {
	    ...
	    private final ExecutorService workerThread = Executors.newSingleThreadExecutor();//單一線程線程池;
	
	    @Override
	    public void touch(File file) throws IOException {
			//異步提交,保存文件;
	        workerThread.submit(new TouchCallable(file));
	    }
	
	    private void touchInBackground(File file) throws IOException {
			//修改file 更改時間; 用於Lru算法判斷最近使用;
	        Files.setLastModifiedNow(file);
			//獲取指定文件夾中文件,按修改時間排序,時間小的放前面,按照從小到大排序;
	        List<File> files = Files.getLruListFiles(file.getParentFile());
	        trim(files);
	    }
	
		//抽象方法,用於判斷以 文件大小 還是 文件數目 作爲lru刪除條件;
	    protected abstract boolean accept(File file, long totalSize, int totalCount);
	
	    private void trim(List<File> files) {
	        long totalSize = countTotalSize(files);
	        int totalCount = files.size();
	        for (File file : files) {
				//先遍歷的是時間小的,就是比較舊的數據,可優先刪除;
	            boolean accepted = accept(file, totalSize, totalCount);
	            if (!accepted) {
	                long fileSize = file.length();
	                boolean deleted = file.delete();
	                if (deleted) {
	                    totalCount--;
	                    totalSize -= fileSize;
	                    LOG.info("Cache file " + file + " is deleted because it exceeds cache limit");
	                } else {
	                    LOG.error("Error deleting file " + file + " for trimming cache");
	                }
	            }
	        }
	    }
	
	    private long countTotalSize(List<File> files) {
	        long totalSize = 0;
	        for (File file : files) {
	            totalSize += file.length();
	        }
	        return totalSize;
	    }
	
	    private class TouchCallable implements Callable<Void> {
	
	        private final File file;
	
	        public TouchCallable(File file) {
	            this.file = file;
	        }
	
	        @Override
	        public Void call() throws Exception {
	            touchInBackground(file);
	            return null;
	        }
	    }
	}
	
	//sourceInfoStorage 數據本地存儲的默認配置:由簡單工廠`SourceInfoStorageFactory.newSourceInfoStorage(context)`獲取
	public interface SourceInfoStorage {
	    SourceInfo get(String url);
	    void put(String url, SourceInfo sourceInfo);
	    void release();
	}

	//使用sqlite 作爲數據存儲; 
	class DatabaseSourceInfoStorage extends SQLiteOpenHelper implements SourceInfoStorage{
		private static final String TABLE = "SourceInfo";
	    private static final String COLUMN_ID = "_id";
	    private static final String COLUMN_URL = "url";
	    private static final String COLUMN_LENGTH = "length";
	    private static final String COLUMN_MIME = "mime";
	    private static final String[] ALL_COLUMNS = new String[]{COLUMN_ID, COLUMN_URL, COLUMN_LENGTH, COLUMN_MIME};
	    private static final String CREATE_SQL =
	            "CREATE TABLE " + TABLE + " (" +
	                    COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
	                    COLUMN_URL + " TEXT NOT NULL," +
	                    COLUMN_MIME + " TEXT," +
	                    COLUMN_LENGTH + " INTEGER" +
	                    ");";
		...
	}
	 
	//headerInjector 添加請求頭的默認配置: 
	public class EmptyHeadersInjector implements HeaderInjector {
	    @Override
	    public Map<String, String> addHeaders(String url) {
	        return new HashMap<>();
	    }
	
	}
	

tips: 注意 Files.setLastModifiedNow(file); 最後更改時間作爲Lru的判斷標準; 這裏兼容時間更改的判斷;
RandomAccessFile mode

	
	static void setLastModifiedNow(File file) throws IOException {
        if (file.exists()) {
            long now = System.currentTimeMillis();
			//更改最後修改時間;
            boolean modified = file.setLastModified(now); // on some devices (e.g. Nexus 5) doesn't work
            if (!modified) {
                modify(file);
                if (file.lastModified() < now) {
                    // NOTE: apparently this is a known issue (see: http://stackoverflow.com/questions/6633748/file-lastmodified-is-never-what-was-set-with-file-setlastmodified)
                    LOG.warn("Last modified date {} is not set for file {}", new Date(file.lastModified()), file.getAbsolutePath());
                }
            }
        }
    }
	//如果更改不成功,則採用`rwd`更新文件內容,寫入文件最後一個byte;
    static void modify(File file) throws IOException {
        long size = file.length();
        if (size == 0) {
            recreateZeroSizeFile(file);
            return;
        }

        RandomAccessFile accessFile = new RandomAccessFile(file, "rwd");
        accessFile.seek(size - 1);
        byte lastByte = accessFile.readByte();
        accessFile.seek(size - 1);
        accessFile.write(lastByte);
        accessFile.close();
    }

數據展示Demo

代碼邏輯理清之後,在看數據就會更爲清晰了;

//ping 請求,serverSocket 收到的信息:
在這裏插入圖片描述

//videoUrl 請求, serverSocket 收到的信息:
在這裏插入圖片描述

當啓動ServerSocket後,本地發出ping請求檢測是否存活,請求信息爲:


	GET /ping HTTP/1.1
	User-Agent: Dalvik/2.1.0 (Linux; U; Android 5.1; m1 note Build/LMY47D)
	Host: 127.0.0.1:49361
	Connection: Keep-Alive
	Accept-Encoding: gzip

videoUrl請求信息爲:

	
	GET /http%3A%2F%2Fjzvd.nathen.cn%2F342a5f7ef6124a4a8faf00e738b8bee4%2Fcf6d9db0bd4d41f59d09ea0a81e918fd-5287d2089db37e62345123a1be272f8b.mp4 HTTP/1.1
	User-Agent: stagefright/1.2 (Linux;Android 5.1)
	key: value
	Host: 127.0.0.1:59689
	Connection: Keep-Alive
	Accept-Encoding: gzip

測試中原videoUrl 路徑爲:

http://jzvd.nathen.cn/342a5f7ef6124a4a8faf00e738b8bee4/cf6d9db0bd4d41f59d09ea0a81e918fd-xxxxx.mp4

isAlive 經過本地代理後的videoUrl(appendToProxyUrl方法) 路徑爲:

http://127.0.0.1:43108/http%3A%2F%2Fjzvd.nathen.cn%2F342a5f7ef6124a4a8faf00e738b8bee4%2Fcf6d9db0bd4d41f59d09ea0a81e918fd-xxxxx.mp4

評估

首先作者基礎功非常紮實, 對http請求,proxy代理,以及流數據的處理封裝都非常棒,非常值得學習的一個視頻緩存框架;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章