文章目錄
- 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代理,以及流數據的處理封裝都非常棒,非常值得學習的一個視頻緩存框架;