Android 视频缓存框架 AndroidVideoCache 用法
一、基本原理
AndroidVideoCache 通过代理的策略将我们的网络请求代理到本地服务,本地服务再决定是从本地缓存拿还是发起网络请求,如果需要发起网络请求就先向本地写入数据,再从本地提供数据给视频播放器。这样就做到了数据的复用。
图
在视频播放器,比如VideoView发起一个urlA,通过HttpProxyCacheServer转成一个本地host和端口的urlB,这样视频播放器发起请求就是向HttpProxyCacheServer请求,返回视频播放器的Socket,Server再建立一个HttpProxyCacheServerClients来发起网络请求处理缓存等工作,然后把数据通过前面的Socket返回给视频播放器。
二、用法
1、导入依赖包
dependencies {
compile 'com.danikula:videocache:2.7.1'
}
2、初始化代理服务器
在全局初始化一个本地代理服务器,这里选择在 Application 的实现类中
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() {
return new HttpProxyCacheServer(this);
}
}
3、使用方法及详解
有了代理服务器,我们就可以使用了,把自己的网络视频 url 用提供的方法替换成另一个 URL
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
HttpProxyCacheServer proxy = getProxy();
String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
videoView.setVideoPath(proxyUrl);
}
提供了更多的可以自定义的地方,比如缓存的文件最大大小,以及文件个数,缓存采取的是 LruCache 的方法,对于老文件在达到上限后会自动清理。
private HttpProxyCacheServer newProxy() {
return new HttpProxyCacheServer.Builder(this)
.maxCacheSize(1024 * 1024 * 1024) // 1 Gb for cache
.build();
}
private HttpProxyCacheServer newProxy() {
return new HttpProxyCacheServer.Builder(this)
.maxCacheFilesCount(20)
.build();
}
除了这个,还有一个就是生成的文件名,默认是使用的 MD5 方式生成 key,考虑到一些业务逻辑,我们也可以继承一个 FileNameGenerator 来实现自己的策略
public class MyFileNameGenerator implements FileNameGenerator {
// Urls contain mutable parts (parameter 'sessionToken') and stable video's id (parameter 'videoId').
// e. g. http://example.com?videoId=abcqaz&sessionToken=xyz987
public String generate(String url) {
Uri uri = Uri.parse(url);
String videoId = uri.getQueryParameter("videoId");
return videoId + ".mp4";
}
}
...
HttpProxyCacheServer proxy = HttpProxyCacheServer.Builder(context)
.fileNameGenerator(new MyFileNameGenerator())
.build()
很明显,构造Server是通过建造者的模式,看下Builder的代码就知道支持哪些配置和默认配置是什么了。
private static final long DEFAULT_MAX_SIZE = 512 * 1024 * 1024;
private File cacheRoot;
private FileNameGenerator fileNameGenerator;
private DiskUsage diskUsage;
private SourceInfoStorage sourceInfoStorage;
private HeaderInjector headerInjector;
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();
this.headerInjector = new EmptyHeadersInjector();
}
cacheRoot就是缓存默认的文件夹,如果有sd卡并且申请了权限,会放到目录("/Android/data/[app_package_name]/cache")
否则放到手机的内部存储
cacheDirPath = "/data/data/" + context.getPackageName() + "/cache/";
FileNameGenerator用于生成文件名,默认是 Md5FileNameGenerator,生成MD5串作为文件名。
DiskUsage是用于管理本地缓存,默认是通过文件大小进行管理,大小默认是512M
private static final long DEFAULT_MAX_SIZE = 512 * 1024 * 1024;
this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE);
SourceInfoStorage是用于存储SourInfo,默认是数据库存储
this.sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(context);
public static SourceInfoStorage newSourceInfoStorage(Context context) {
return new DatabaseSourceInfoStorage(context);
}
那SourInfo是什么?主要用于存储http请求源的一些信息,比如url,数据长度length,请求资源的类型mime:
public final String url;
public final long length;
public final String mime;
HeaderInjector主要用于添加一些自定义的头部字段,默认是空
this.headerInjector = new EmptyHeadersInjector();
最后把这些字段构造成Config,构造HttpProxyCacheServer需要,后面会再传给HttpProxyCacheServerClients用于发起请求(url,length,mime)等,和本地缓存(DiskUsage,SourceInfoStorage,cacheRoot)等。
/**
* Builds new instance of {@link HttpProxyCacheServer}.
*
* @return proxy cache. Only single instance should be used across whole app.
*/
public HttpProxyCacheServer build() {
Config config = buildConfig();
return new HttpProxyCacheServer(config);
}
private Config buildConfig() {
return new Config(cacheRoot, fileNameGenerator, diskUsage, sourceInfoStorage, headerInjector);
}
三、AndroidVideoCache 的不足
1、Seek 的场景
播放器 Seek 后有可能就不缓存了。
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.
return !sourceLengthKnown || !request.partial || request.rangeOffset <= cacheAvailable + sourceLength * NO_CACHE_BARRIER;
}
这个不符合我们的预期,seek后也应该进行缓存,这是缓存文件之间可能存在空洞,需要针对这种情况做些特殊处理。
2、预缓存(脱离播放器实现缓存)
提前下载,无论视频是否下载完成,都可以将这提前下载好的部分作为视频缓存使用,进行下扩展。根据url创建GetRequest,然后调用HttpProxyCacheServerClients#processRequest即可
HttpProxyCacheServerClients clients = getClients(url);
clients.processRequest(request);
3、线程管理
开启线程过多,过多线程的内存消耗以及状态同步是一个需要注意点。可以把线程改为线程池的方式实现。但是要特别并发和状态同步。
HttpProxyCacheServer.WaitRequestsRunnable—》等待socket连接
HttpProxyCacheServer.SocketProcessorRunnable—》处理单个socket连接
ProxyCache.SourceReaderRunnable —>分块(8192个字节)读取网络数据流写入到缓存文件并且返回给clientSocket 【这个线程要重点分析】
4、缓存是根据url来进行区分,对于大的视频,没有进行分片下载,节省流量
可以参考m3u8的方式,给一个视频进行分片。这个后面再分析另外一个开源项目是再来一些拆解。