如今更新文章的速度愈来愈慢了......😄,就是太懒了......html
由于最近项目正好涉及到音乐播放器的音频缓存,固然咱们要作的第一步固然是百度或者谷歌经常使用的缓存库,起码我是不肯意本身写的,多麻烦!!! 百度之后:git
出来的结果都是关于这个库的,恩,那就直接用这个库了: AndroidVideoCachegithub
固然咱们虽然不去本身写,简单使用,可是咱们仍是要懂原理,这个可不能偷懒,并且看了源码也方便咱们本身去针对性定制化。web
咱们先来看简单的使用:缓存
//'通常咱们会在Application里面初始化, 主要的目的是为了经过单例模式使用HttpProxyCacheServer:'
public class App extends Application {
private HttpProxyCacheServer proxy;
public static HttpProxyCacheServer getProxy(Context context) {
App app = (App) context.getApplicationContext();
return app.proxy == null ? (app.proxy = app.newProxy()) : app.proxy;
}
private HttpProxyCacheServer newProxy() {
//'咱们实例化HttpProxyCacheServer的对象'
return new HttpProxyCacheServer(this);
}
}
//'而后业务代码中使用,把原始的url传入,得到到通过代理的url'
HttpProxyCacheServer proxy = getProxy();
String proxyUrl = proxy.getProxyUrl(url);
//'把代理url给你的代码使用:'
//好比给exoplayer使用
MediaSource mediaSource = buildMediaSource(Uri.parse(proxyUrl));
mExoPlayer.prepare(mediaSource);
//好比给videoView使用
videoView.setVideoPath(proxyUrl);
复制代码
没错,就是这么简单,你只要传的url每次都是同样的,就能够直接去获取本地缓存文件,而后去播放,不须要再去浪费流量请求了。bash
须要定制化的地方:服务器
可是咱们能够看到咱们只传入了一个url,说明内部是把url做为Key去获取对应的内容的。通常的歌曲都没问题,好比你要播放XXX歌,获取大家公司的服务器的某个内容,url通常不会改变,可是咱们公司和和其余公司音乐CP方合做,因此获取具体的音乐的url每次获取都会改变,就是同一首歌,相隔一秒请求,请求的播放歌曲的url也会改变,好比http://歌曲id号/23213213213123/xxxx.mp3
,好比中间的数字是根据请求的时间的时间戳拼接返回的,因此每次都会不一样,这时候你直接使用这个库,就会出问题,你会发现缓存没有任何软用。websocket
因此针对这种状况:cookie
http://歌曲id号
),这样去比较本地是否有缓存文件的时候,就不会有问题了。无论用哪一种,咱们均可以借机去了解源码。网络
咱们看到了咱们要使用这个库,其实就是实例化这个HttpProxyCacheServer
类,而后使用它的getProxyUrl
方法获取代理url
便可。
那咱们就知道了这个是代理总的管理类了。咱们看下实例化它对象的时候,内部有什么:
public HttpProxyCacheServer(Context context) {
//'默认使用特定的Config配置'
this(new Builder(context).buildConfig());
}
private HttpProxyCacheServer(Config config) {
//'Config配置'
this.config = checkNotNull(config);
try {
//'建立了本地的ServerSocket,做为中转做用'
InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
this.serverSocket = new ServerSocket(0, 8, inetAddress);
this.port = serverSocket.getLocalPort();
//'建立代理选择器,从而若是是其余网络请求,继续通过原来的代理,若是是本身设置的ServerSocker,则不通过代理'
IgnoreHostProxySelector.install(PROXY_HOST, port);
CountDownLatch startSignal = new CountDownLatch(1);
//'启动了本地的ServerSocket,开启接受外部访问的Socket'
this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
this.waitConnectionThread.start();
startSignal.await(); // freeze thread, wait for server starts
//'自定义的Pinger类主要用来等会模拟访问本地ServerSocket请求,确保请求网络没问题'
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);
}
}
复制代码
题外话开始!!:
不少人和可能对于ServerSocket , ProxySelector等都比较迷糊,由于不少人网络请求直接使用了Okhttp等直接封装好的东西,因此对于Socket,Proxy/ProxySelector等反而比较模糊。
对于网络基础能够看我之前写的文章:
Android技能树 — 网络小结(3)之HTTP/HTTPS
Android技能树 — 网络小结(4)之socket/websocket/webservice
相关网络知识点小结- cookie/session/token(待写)
Android技能树 — 网络小结(6)之 OkHttp超超超超超超超详细解析
Android技能树 — 网络小结(7)之 Retrofit源码详细解析
当前简单的想知道 Socket和ServerSocket和两者的使用,也能够看下面这篇:
其中代理相关的Proxy和ProxySelector,能够看下面这篇:
代理服务器:Proxy(代理链接)、ProxySelector(自动代理选择器)、默认代理选择器
题外话结束!!
咱们来看下Config都包含了什么:
class Config {
//缓存文件的目录
public final File cacheRoot;
//缓存文件的命名
public final FileNameGenerator fileNameGenerator;
//磁盘使用统计类(好比设置的缓存文件数仍是缓存文件空间)
public final DiskUsage diskUsage;
//存了资源的相关信息:url/length/mime
public final SourceInfoStorage sourceInfoStorage;
//网络请求,能够插入Header信息
public final HeaderInjector headerInjector;
......
......
......
}
复制代码
咱们看了实例化对象,接下去就是经过调用getProxyUrl获取代理url了,咱们看下具体作了什么事。
public String getProxyUrl(String url) {
return getProxyUrl(url, true);
}
public String getProxyUrl(String url, boolean allowCachedFileUri) {
//'Boolean参数是否容许缓存 && isCached判断传入的url是否已经有缓存了'
if (allowCachedFileUri && isCached(url)) {
//'获取缓存文件'
File cacheFile = getCacheFile(url);
//'由于取了这个缓存文件,因此把这个缓存文件的修改时间改成当前时间,'
//'同时对全部的缓存文件从新根据修改时间进行排序'
touchFileSafely(cacheFile);
//'把本地文件的url返回给上层使用'
return Uri.fromFile(cacheFile).toString();
}
//'isAlive()的做用是建立http://127.0.0.1:port/ping 网络请求,判断本地的ServerSocket是否还能链接访问, //若是能够链接,就把传进来的url变为代理服务器ServerSokcet的url (http://127.0.0.1:port/[url]), //若是本地ServerSocket已经挂了,就直接把原来的url返回给上层'
return isAlive() ? appendToProxyUrl(url) : url;
}
复制代码
这里要注意一个小细节,就是
appendToProxyUrl
里面不是单纯的把咱们实际的url拼接在http://127.0.0.1:port
后面。好比咱们的实际url是http://xxxxxx.mp3
,不是拼接成:http://127.0.0.1:port/http://xxxxx.mp3
,这样一看这个网址就是有问题的。而是把咱们的实际url先经过utf-8转移成其余字符:URLEncoder.encode(url, "utf-8");
而后拼接上去,最后结果为:http://127.0.0.1:50544/http%3A%2F%2Fxxxxxx.mp3
。要使用实际url的时候,拿出来再反过来解析就行:URLDecoder.decode(url, "utf-8");
那咱们确定着重看下第二种状况,也就是本地没有缓冲,你这个url是第一次传进来的时候的状况。
咱们上面已经经过getProxyUrl
获取到了新的而且过的url: http://127.0.0.1:port/[实际访问的url]
, 这时候咱们用ExoPlayer 或者VideoView等去访问这个网址的时候,变成了访问本地服务器ServerSocket
了(本地ServerSocket
就是建立的127.0.0.1
)。
咱们来详细看ServerSocket
接受请求的相关代码:
private void waitForRequest() {
try {
while (!Thread.currentThread().isInterrupted()) {
//'咱们拿到的proxyUrl访问后,ServerSocket接受到了咱们的请求Socket'
Socket socket = serverSocket.accept();
//'咱们能够看到拿着咱们的请求Socket去执行Runnable'
socketProcessor.submit(new SocketProcessorRunnable(socket));
}
} catch (IOException e) {
onError(new ProxyCacheException("Error during waiting connection", e));
}
}
复制代码
咱们来看这个Runaable
作了什么:
private final class SocketProcessorRunnable implements Runnable {
private final Socket socket;
public SocketProcessorRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//'这个Runnable执行了下面的processSocket方法'
processSocket(socket);
}
}
private void processSocket(Socket socket) {
try {
//'获取咱们的Socket的InputStream,而后传入获取GetRequest对象'
GetRequest request = GetRequest.read(socket.getInputStream());
//'获取到咱们的Socket请求的url: http://127.0.0.1:port/[实际访问的url]'
String url = ProxyCacheUtils.decode(request.uri);
//'这个url是不是用来ping的请求地址 (是否记得咱们前面isAlive()方法,ping一下本地ServerSocket,看是否存活)'
if (pinger.isPingRequest(url)) {
//'若是只是简单的ping的请求,就简单的处理回复'
pinger.responseToPing(socket);
} else {
//'进入这里,说明这个url是http://127.0.0.1:port/[实际访问的url], 咱们根据url来获取HttpProxyCacheServerClients对象,而后执行接下去的步骤'
HttpProxyCacheServerClients clients = getClients(url);
clients.processRequest(request, socket);
}
} catch (SocketException e) {
......
......
} catch (ProxyCacheException | IOException e) {
......
......
} finally {
//'释放咱们的请求Socket'
releaseSocket(socket);
}
}
复制代码
咱们分步来看上面的操做后,确定有这些疑问:
在分析接下去流程以前,咱们先来看看这二个类是作什么的。
'假设实际的网络请求是 http://xxxxxx.mp3 代理后的是http://127.0.0.1:50544/http%3A%2F%2Fxxxxxx.mp3'
//'类的代码不多,其实就是咱们的Socket请求本地ServerSocket的时候, //获取到的请求Socket的InputStream中能够读取到如下的请求内容(): GET /http%3A%2F%2Fxxxxxx.mp3 HTTP/1.1 (PS:前面咱们还记得alive()方法来进行ping的时候,那么这里的就会是GET /ping HTTP/1.1) User-Agent: Dalvik/2.1.0 (Linux; U; Android 6.0.1; MuMu Build/V417IR) Host: 127.0.0.1:50276 Connection: Keep-Alive Accept-Encoding: gzip (Range: bytes=0-10 若是网络请求有range,这里就会有该内容,好比分段下载时候,参考:https://www.cnblogs.com/1995hxt/p/5692050.html) '
class GetRequest {
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;
public GetRequest(String request) {
checkNotNull(request);
long offset = findRangeOffset(request);
this.rangeOffset = Math.max(0, offset);
this.partial = offset >= 0;
this.uri = findUri(request);
}
public static GetRequest read(InputStream inputStream) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
StringBuilder stringRequest = new StringBuilder();
String line;
while (!TextUtils.isEmpty(line = reader.readLine())) { // until new line (headers ending)
stringRequest.append(line).append('\n');
}
return new GetRequest(stringRequest.toString());
}
private long findRangeOffset(String request) {
//'获取range值'
Matcher matcher = RANGE_HEADER_PATTERN.matcher(request);
if (matcher.find()) {
String rangeValue = matcher.group(1);
return Long.parseLong(rangeValue);
}
return -1;
}
private String findUri(String request) {
//'经过Matcher匹配,获取GET /http%3A%2F%2Fxxxxxx.mp3 HTTP/1.1 里面的中间的http%3A%2F%2Fxxxxxx.mp3'
Matcher matcher = URL_PATTERN.matcher(request);
if (matcher.find()) {
return matcher.group(1);
}
throw new IllegalArgumentException("Invalid request `" + request + "`: url not found!");
}
@Override
public String toString() {
return "GetRequest{" +
"rangeOffset=" + rangeOffset +
", partial=" + partial +
", uri='" + uri + '\'' + '}'; } } 复制代码
因此该类的做用就是把本次发送到本地ServerSocket的请求中,拿到相关的请求里面的参数,因此该类里面包含了下面参数:
咱们再看看看这个类:
final class HttpProxyCacheServerClients {
private final AtomicInteger clientsCount = new AtomicInteger(0);
//'咱们看到了url: http://127.0.0.1:port/[实际url]'
private final String url;
//'看到了HttpProxyCache这个类,这个类是作什么的????从字面意思是网络代理缓存类,后续继续细看'
private volatile HttpProxyCache proxyCache;
//'看到了一系列的监听器,从字面意思就知道是缓存监听器,当注册了这块的缓存监听,后续缓存好了能够通知'
private final List<CacheListener> listeners = new CopyOnWriteArrayList<>();
private final CacheListener uiCacheListener;
//'这个前面说过,咱们的Config配置,包括缓存路径等'
private final Config config;
public HttpProxyCacheServerClients(String url, Config config) {
this.url = checkNotNull(url);
this.config = checkNotNull(config);
this.uiCacheListener = new UiListenerHandler(url, listeners);
}
//'进行请求'
public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException {
//'实例化一个HttpProxyCache对象'
startProcessRequest();
try {
clientsCount.incrementAndGet();
//'使用HttpProxyCache对象拿着GetRequest和Socket对象,进行下一步操做'
proxyCache.processRequest(request, socket);
} finally {
finishProcessRequest();
}
}
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对象,须要传入HttpUrlSource对象和FileCache对象'
HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache);
httpProxyCache.registerCacheListener(uiCacheListener);
return httpProxyCache;
}
......
......
......
}
复制代码
因此简单的显示就是这样:
因此咱们就知道了HttpProxyCacheServerSocket
的做用了,一个url
对应了一个HttpProxyCacheServerSocket
,也就对应了:
FileCache
管理了存储空间地址,存储状态的判断(好比是否存储空间满了)HttpUrlSource
管理了相关的网络请求模块并把数据下载到FileCache
处,因此咱们具体一个个来看。
public class FileCache implements Cache {
......
......
......
}
'咱们能够看到它是实现了Cache类,因此咱们只要看接口定义了哪些方法,就知道了FileCache的具体功能'
public interface Cache {
//'返回当前文件的大小:file.getLength();'
long available() throws ProxyCacheException;
//'由于使用的是RandomAccessFile,因此能够跳到File中的某一段在读取,而不是必定要从头开始'
//'offset偏移值,length读取长度'
int read(byte[] buffer, long offset, int length) throws ProxyCacheException;
//'一边云端获取数据,一边往文件后续继续添加内容(下载完就不日后面添加)'
//'data传入的数据 , length 当前文件的内容长度'
void append(byte[] data, int length) throws ProxyCacheException;
//'下载完后须要作的操做,1.文件数据流关闭,2.使用DisUsage对缓存文件进行从新排序,清除不须要的缓存文件'
void close() throws ProxyCacheException;
//'下载完成后,把文件的后缀名.download改成真正的文件后缀名,好比mp3'
//'(这个后缀名是根据传入的url里面拿的,通常好比http://xxxxxxxx.mp3,就拿到了mp3后缀)'
void complete() throws ProxyCacheException;
//'判断是不是临时文件,根据文件后缀名是否是.download判断'
//'(就像咱们日常下东西,下的时候是xxxx.yyyyy某个临时文件,下载完后才是正确的格式,好比是xxxx.mp3)'
boolean isCompleted();
}
复制代码
因此整体来讲是把云端的数据缓存到本地等一系列操做。
咱们来看它的代码:
public class HttpUrlSource implements Source {
......
......
......
}
public interface Source {
//'打开网络链接,得到了HttpURLConnection对象'
void open(long offset) throws ProxyCacheException;
//'获取网络资源的内容长度'
long length() throws ProxyCacheException;
//'使用HttpURLConnection读取相应的返回内容'
int read(byte[] buffer) throws ProxyCacheException;
//'关闭HttpURLConnection'
void close() throws ProxyCacheException;
}
复制代码
是否是看着就很清晰了,该类用来进行网络链接,而后读取网络数据暴露给外部。
这里要注意一个小细节:
而HttpUrlSource中网络请求回来的数据后面有二种方式提供:
也就是HttpProxyCache
类里面的这段代码:
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 {
//直接将云端读取的内容返回给Socket
responseWithoutCache(out, offset);
}
}
复制代码
咱们确定是具体想看的是缓存读取的流程。因此咱们上面大体的代码已经写过了,如今再回头看看具体每一步是怎么实现的。
咱们已经知道是在HttpUrlSource
里面:
@Override
public void open(long offset) throws ProxyCacheException {
try {
//'openConnection打开网络链接'
connection = openConnection(offset, -1);
//'获取当前访问的数据类型格式'
String mime = connection.getContentType();
//'获取当前链接的输出流'
inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE);
//'获取当前返回数据的总长度'
long length = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
//'获取到的内容都使用SourceInfo来进行存储管理'
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);
}
}
//'网络链接就是使用基础的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();
injectCustomHeaders(connection, url);
if (offset > 0) {
connection.setRequestProperty("Range", "bytes=" + offset + "-");
}
if (timeout > 0) {
connection.setConnectTimeout(timeout);
connection.setReadTimeout(timeout);
}
int code = connection.getResponseCode();
redirected = code == HTTP_MOVED_PERM || code == HTTP_MOVED_TEMP || code == HTTP_SEE_OTHER;
if (redirected) {
url = connection.getHeaderField("Location");
redirectCount++;
connection.disconnect();
}
if (redirectCount > MAX_REDIRECTS) {
throw new ProxyCacheException("Too many redirects: " + redirectCount);
}
} while (redirected);
return connection;
}
//'暴露给外部的数据读取方法实际上就是用上面获取到的输出流来读取内容'
@Override
public int read(byte[] buffer) throws ProxyCacheException {
if (inputStream == null) {
throw new ProxyCacheException("Error reading data from " + sourceInfo.url + ": connection is absent!");
}
try {
return inputStream.read(buffer, 0, buffer.length);
} catch (InterruptedIOException e) {
throw new InterruptedProxyCacheException("Reading source " + sourceInfo.url + " is interrupted", e);
} catch (IOException e) {
throw new ProxyCacheException("Error reading data from " + sourceInfo.url, e);
}
}
复制代码
咱们来看HttpProxyCache里面的代码:
//'咋一眼看和responseWithoutCache不是长的如出一辙么关键就是在于读取数据的地方作了中间处理'
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.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);
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;
}
复制代码
因此具体的其实就在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();
}
}
private class SourceReaderRunnable implements Runnable {
@Override
public void run() {
readSource();
}
}
//'实际的方法:'
private void readSource() {
long sourceAvailable = -1;
long offset = 0;
try {
offset = cache.available();
//'前面介绍过HttpUrlSource的,open你们应该知道了。链接网络请求'
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);
}
}
复制代码
因此整个流程就清楚了.......
回到咱们刚开始的问题:
1.咱们能够自定义FileNameGenerator
public class ATFileNameGenerator implements FileNameGenerator {
好比http://aaaaaaa.com/905_xxxxxxxxxxxx.mp3
通常来讲网址里面带了这首歌的id值(好比这里的905),可能后面拼接了其余时间戳等。咱们只要取出来核心的地方就好了:
public String generate(String url) {
// 只有爱听url会变化
if (url.contains("aaaaaa.com")) {
//而后经过大家对应的规则,取出来中间的id值,
//而后用id。来做为文件名字,找的时候也经过这个规则找文件便可。
return musicId;
}
Md5FileNameGenerator md5FileNameGenerator = new Md5FileNameGenerator();
return md5FileNameGenerator.generate(url);
}
}
复制代码
写的烂轻点喷便可.......