前言
YouTube
Android 版用的也是ExoPlayer( Media3) 。看图片,可以看出来,缓存家族成员类不算少了,大约20个类。主要是靠大哥 SimpleCache 控制。让我们来看看!它是如何提供一个缓存和确保一个缓存的可用的。
核心类 | 用途 |
---|---|
CacheDataSink | 负责缓存文件的写入 |
CacheDataSource | 缓存数据源 |
CachedContentIndex | 缓存文件内容索引 |
CacheFileMetadataIndex | 缓存文件数据索引 |
LeastRecentlyUsedCacheEvictor | 缓存LRU算法 |
SimpleCache | 缓存控制主体 |
先从缓存数据库表的设计入手
CachedContentIndex
这个表只有三个字段
- id 自增值
- key 缓存文件源地址
- metadata 视频类型
CacheFileMetadataIndex
这个表也有三个字段
-
name 定义缓存文件的名称(id + "." + position + "." + timestamp + SUFFI),注意文件name,前面的数字,这个name会缓存在TreeSet中,需要排序。缓存的文件是使用这个名称。
-
length 缓存文件的长度
-
last_touch_timestamp 缓存文件上一次访问的时间(可用于LRU算法)
缓存的使用方式
只需要三个参数
1.缓存文件夹
2.缓存算法,缓存大小
3.储存的数据库提供者
val cache = SimpleCache(this.cacheDir, LeastRecentlyUsedCacheEvictor(1024*1024*1024L), StandaloneDatabaseProvider(this))
initialize() 缓存初始化
核心是创建缓存文件夹,扫描缓存文件,加载索引到内存中。
- 1,创建缓存文件夹
- 2,加载缓存文件
- 3,加载缓存文件UID 标识,没有就创建一个
private void initialize() {
if (!cacheDir.exists()) {
try {
createCacheDirectories(cacheDir);
} catch (CacheException e) {
initializationException = e;
return;
}
}
@Nullable File[] files = cacheDir.listFiles();
if (files == null) {
String message = "Failed to list cache directory files: " + cacheDir;
Log.e(TAG, message);
initializationException = new CacheException(message);
return;
}
uid = loadUid(files);
if (uid == UID_UNSET) {
try {
uid = createUid(cacheDir);
} catch (IOException e) {
String message = "Failed to create cache UID: " + cacheDir;
Log.e(TAG, message, e);
initializationException = new CacheException(message, e);
return;
}
}
try {
contentIndex.initialize(uid);
if (fileIndex != null) {
fileIndex.initialize(uid);
Map<String, CacheFileMetadata> fileMetadata = fileIndex.getAll();
loadDirectory(cacheDir, /* isRoot= */ true, files, fileMetadata);
fileIndex.removeAll(fileMetadata.keySet());
} else {
loadDirectory(cacheDir, /* isRoot= */ true, files, /* fileMetadata= */ null);
}
} catch (IOException e) {
String message = "Failed to initialize cache indices: " + cacheDir;
Log.e(TAG, message, e);
initializationException = new CacheException(message, e);
return;
}
contentIndex.removeEmpty();
try {
contentIndex.store();
} catch (IOException e) {
Log.e(TAG, "Storing index file failed", e);
}
}
获取缓存
1,CachedContent不存在,创建长度占位
2,CachedContent 存在,长度一致
3,CachedContent 存在,还没有缓存
private SimpleCacheSpan getSpan(String key, long position, long length) {
@Nullable CachedContent cachedContent = contentIndex.get(key);
if (cachedContent == null) {
return SimpleCacheSpan.createHole(key, position, length);
}
//确保缓存是null 或者缓存是可用
while (true) {
SimpleCacheSpan span = cachedContent.getSpan(position, length);
if (span.isCached && Assertions.checkNotNull(span.file).length() != span.length) {
//todo:移除length不匹配的缓存
removeStaleSpans();
continue;
}
return span;
}
}
添加缓存
1,添加缓存
2,累计缓存大小
3,回调接口
private void addSpan(SimpleCacheSpan span) {
contentIndex.getOrAdd(span.key).addSpan(span);
totalSpace += span.length;
notifySpanAdded(span);
}
移除一个缓存文件
private void removeSpanInternal(CacheSpan span) {
@Nullable CachedContent cachedContent = contentIndex.get(span.key);
if (cachedContent == null || !cachedContent.removeSpan(span)) {
return;
}
totalSpace -= span.length;
if (fileIndex != null) {
String fileName = Assertions.checkNotNull(span.file).getName();
try {
fileIndex.remove(fileName);
} catch (IOException e) {
// This will leave a stale entry in the file index. It will be removed next time the cache
// is initialized.
Log.w(TAG, "Failed to remove file index entry for: " + fileName);
}
}
contentIndex.maybeRemove(cachedContent.key);
notifySpanRemoved(span);
}
移除文件length不匹配的。
private void removeStaleSpans() {
ArrayList<CacheSpan> spansToBeRemoved = new ArrayList<>();
for (CachedContent cachedContent : contentIndex.getAll()) {
for (CacheSpan span : cachedContent.getSpans()) {
if (Assertions.checkNotNull(span.file).length() != span.length) {
spansToBeRemoved.add(span);
}
}
}
for (int i = 0; i < spansToBeRemoved.size(); i++) {
removeSpanInternal(spansToBeRemoved.get(i));
}
}
缓存唯一标识uid
加载缓存唯一标识uid
加载缓存唯一标识uid,没有设置情况下,会调用createUid 创建一个。
private static long loadUid(File[] files) {
for (File file : files) {
String fileName = file.getName();
if (fileName.endsWith(UID_FILE_SUFFIX)) {
try {
return parseUid(fileName);
} catch (NumberFormatException e) {
file.delete();
}
}
}
return UID_UNSET;
}
创建缓存文件唯一标识uid
1,生成非负UID
2, 保存为文件
3,返回Uid
private static long createUid(File directory) throws IOException {
// 生成非负UID。
long uid = new SecureRandom().nextLong();
uid = uid == Long.MIN_VALUE ? 0 : Math.abs(uid);
// 保存为文件
String hexUid = Long.toString(uid, /* radix= */ 16);
File hexUidFile = new File(directory, hexUid + UID_FILE_SUFFIX);
if (!hexUidFile.createNewFile()) {
// False means that the file already exists, so this should never happen.
throw new IOException("Failed to create UID file: " + hexUidFile);
}
return uid;
}
缓存文件夹,标记上锁
给缓存文件夹,标记上锁,避免同时写同一个文件夹
-
lockFolder
给缓存文件夹,标记上锁,避免同时写同一个文件夹 -
unlockFolder
移除缓存文件夹,即标记解锁
private static synchronized boolean lockFolder(File cacheDir) {
return lockedCacheDirs.add(cacheDir.getAbsoluteFile());
}
private static synchronized void unlockFolder(File cacheDir) {
lockedCacheDirs.remove(cacheDir.getAbsoluteFile());
}
无阻塞获取缓存
- 1,获取缓存基本信息
- 2,如果缓存已缓存,就修改缓存访问时间
- 3,没有缓存,如果上锁了,返回 span.
- 4,缓存不存在,也没有上锁,返回null
public synchronized CacheSpan startReadWriteNonBlocking(String key, long position, long length)
throws CacheException {
Assertions.checkState(!released);
checkInitialization();
SimpleCacheSpan span = getSpan(key, position, length);
if (span.isCached) {
// Read case.
return touchSpan(key, span);
}
CachedContent cachedContent = contentIndex.getOrAdd(key);
if (cachedContent.lockRange(position, span.length)) {
// Write case.
return span;
}
// Lock not available.
return null;
}
阻塞获取缓存
代码核心还是调用 startReadWriteNonBlocking
实现,就是多了阻塞。从 CacheDataSource
中,可以看出 openNextSource
看出 当 blockOnCache
为true时候,会调用这个方法。FLAG_BLOCK_ON_CACHE
: 如果缓存被锁定,不管数据是否被缓存,都从上游读取。 下载器会使用到这个 缓存flag
。
public synchronized CacheSpan startReadWrite(String key, long position, long length)
throws InterruptedException, CacheException {
Assertions.checkState(!released);
checkInitialization();
while (true) {
CacheSpan span = startReadWriteNonBlocking(key, position, length);
if (span != null) {
return span;
} else {
......
wait();
}
}
}
提交缓存文件
1,创建一个SimpleCacheSpan
2,检查span是否与设置的内容长度冲突
3,写入数据库,缓存索引(缓存到文件夹下的文件使用的是这里的name)
4,添加到内存缓存中
5,写入缓存文件索引到数据库
6,唤醒线程,startReadWrite 中的等待被唤醒。
public synchronized void commitFile(File file, long length) throws CacheException {
......
SimpleCacheSpan span =
Assertions.checkNotNull(SimpleCacheSpan.createCacheEntry(file, length, contentIndex));
CachedContent cachedContent = Assertions.checkNotNull(contentIndex.get(span.key));
Assertions.checkState(cachedContent.isFullyLocked(span.position, span.length));
long contentLength = ContentMetadata.getContentLength(cachedContent.getMetadata());
if (contentLength != C.LENGTH_UNSET) {
Assertions.checkState((span.position + span.length) <= contentLength);
}
if (fileIndex != null) {
String fileName = file.getName();
try {
//写入数据库
fileIndex.set(fileName, span.length, span.lastTouchTimestamp);
} catch (IOException e) {
throw new CacheException(e);
}
}
//添加到内存中
addSpan(span);
try {
//写入缓存文件索引到数据库
contentIndex.store();
} catch (IOException e) {
throw new CacheException(e);
}
notifyAll();
}
释放缓存坑位
释放从startReadWrite
获得的CacheSpan
,该CacheSpan对应于缓存中一个坑位。在CacheDataSource
里面调用,有三处地方,分别是 cacheWriteDataSource 不等于null,抛出异常时,在 closeCurrentSource中调用。
@Override
public synchronized void releaseHoleSpan(CacheSpan holeSpan) {
Assertions.checkState(!released);
CachedContent cachedContent = Assertions.checkNotNull(contentIndex.get(holeSpan.key));
cachedContent.unlockRange(holeSpan.position);
contentIndex.maybeRemove(cachedContent.key);
notifyAll();
}
下一篇我们详细分析一下 CacheDataSource 吧!打通缓存数据源的获取
参考资料
ExoPlayer架构详解与源码分析(12)——Cache
本人知识有限,如有描述错误之处,望虎正。
你的赞就像冬日暖阳,温暖心窝。