系列文章目录
ExoPlayer架构详解与源码分析(1)——前言
ExoPlayer架构详解与源码分析(2)——Player
ExoPlayer架构详解与源码分析(3)——Timeline
ExoPlayer架构详解与源码分析(4)——整体架构
ExoPlayer架构详解与源码分析(5)——MediaSource
ExoPlayer架构详解与源码分析(6)——MediaPeriod
ExoPlayer架构详解与源码分析(7)——SampleQueue
ExoPlayer架构详解与源码分析(8)——Loader
ExoPlayer架构详解与源码分析(9)——TsExtractor
ExoPlayer架构详解与源码分析(10)——H264Reader
ExoPlayer架构详解与源码分析(11)——DataSource
ExoPlayer架构详解与源码分析(12)——Cache
前言
上篇介绍完基本的DataSource,现在可以开始CacheDataSource和TeeDataSource了。
先看下整体结构:
上图这里假设CacheDataSource原始的上游数据是通过OkHttpDataSource从网络获取
看完上图,是不是感觉非常复杂,没关系我们可以拆解出几个独立的结构一步步了解,可以看到底层的Cache可以作为一个独立的结构,在说CacheDataSource和TeeDataSource前,先把Cache这个基础先了解下。
Cache
可以将资源分段的缓存,资源指的是一个完整的媒体文件(如一个MP4,ts文件),每个资源都有唯一的key,一般使用资源的URI作为Key,有时候同一个资源会有不同的URI(如URI加上了失效时间)这种情况就不适合作为资源的 key了。一个资源由多个CacheSpan组成,CacheSpan包含一个数据起始位置和一个长度,代表了资源中的一段数据,CacheSpan并不一定会被Cache,当没有被Cache时叫做HoleSpan,如果被Cached CacheSpan就会对应一个缓存文件。
下面看下具体方法:
-
getUid 返回缓存的非负唯一标识符,如果在确定唯一标识符之前初始化失败,则返回UID_UNSET 。一个缓存目录对应一个UID,SimpleCache会在缓存目录下创建一个UID的文件用于下次读取UID。
-
release 释放缓存。当不再需要缓存时必须调用此方法。调用此方法后不得使用缓存。此方法可能很慢,通常不应在主线程上调用。
-
getCachedSpans 返回给定资源key的所有CacheSpan。
-
getKeys 返回所有有缓存的资源key。
-
getCacheSpace 返回所有缓存所占磁盘空间大小。
-
startReadWrite 通过传入的资源key获取资源,再通过postion和length获取指定的CacheSpan,当调用DataSource open数据源的时候应该同步调用此方法。
- 如果指定位置存在已经缓存的数据也就是CacheSpan.isCached为 true,则返回的CacheSpan.file有值,表示当前缓存的文件。
- 如果没有查询到缓存的CacheSpan则返回一个空的HoleSpan,当调用者从上游获取到数据时可以向当前的HoleSpan指定的范围写入数据,写入完成前此段HoleSpan指定的范围将会锁定,此时再通过startReadWrite会阻塞,当写入完成时,应该通过调用commitFile(File, long)会创建一个已缓存的Span提交到缓存中,此时之前阻塞的startReadWrite将会唤醒,可以获取到一个已缓存的CacheSpan,当调用者完成写入后,必须通过调用releaseHoleSpan来释放锁,此时startReadWrite可以正常获取到已缓存的CacheSpan。此方法可能会阻塞,通常不应在主线程上调用。
- 入参length表示所请求数据的长度,如果未知则为C.LENGTH_UNSET,如果存在与该postion重叠的缓存条目,则忽略该长度,也就是入参position 的查找优先级高于length,startReadWrite通常被用于后台下载器,当下载器要下载的数据段此时正在被缓存,会等待缓存完成。
-
startReadWriteNonBlocking 和startReadWrite类似,不同的是当DataSource被锁定的时候,不会阻塞会直接返回null,startReadWriteNonBlocking主要是播放器使用,因为播放器是不允许阻塞的,在缓存未获取到时会直接跳过缓存。
-
startFile 获取可写入数据的缓存文件。必须先调用startReadWrite(String, long, long)获得的相应HoleSpan时才能调用。不应在主线程上调用。
-
commitFile 将文件提交到缓存中。必须先调用startReadWrite(String, long, long)获得的相应HoleSpan时才能调用。不应在主线程上调用。
-
releaseHoleSpan 释放从startReadWrite(String, long, long)获得的 HoleSpan。
-
removeResource 删除资源的所有CacheSpans ,同时删除底层文件。
-
removeSpan 从缓存中删除缓存的CacheSpan ,从而删除底层文件。不应在主线程上调用。
-
isCached 返回资源中指定范围的数据是否已完全缓存。
-
getCachedLength 返回从资源的position开始,直到最大maxLength的连续缓存数据的长度。如果未缓存position ,则返回-holeLength ,其中holeLength是从position开始,直到最大值maxLength的连续未缓存数据的长度。
-
getCachedBytes 返回资源position (包含)和(position + length) (不包含)之间的缓存字节总数。
-
applyContentMetadataMutations 存储资源相关的Meta信息如资源的总长度 。不应在主线程上调用。
-
getContentMetadata 获取资源的Meta信息。
一个新的缓存添加时,Cache的一般执行顺序是:
- startReadWrite获取HoleSpan,同时锁定这段HoleSpan,防止其他线程再次获取这段HoleSpan。
- startFile获取CacheSpan对应的文件。
- 对2获取的文件进行写入操作。
- commitFile提交写入的文件,并创建与HoleSpan一致的已缓存的CacheSpan提交到Span索引,此时其他线程startReadWrite唤醒可以获取到一个CacheSpan供读取。
- releaseHoleSpan释放startReadWrite获取HoleSpan,此时其他线程可以再次startReadWrite获取到一个HoleSpan,并再次写入数据。
继续深挖,在讲Cache实现前说下其他几个类
DataSink
这是一个用来向其中写入数据的组件,概念上是和DataSource完全相反,提供了write供外部写入数据
看下主要方法:
- open 打开一个数据源,以用来写入指定的数据,同样传入一个DataSpec参照DataSource。
- write 消费掉传入的数据,用法上和DataSource的read类型,不过这里传入的buffer是用来读取的。
- close 关闭源。即使open调用抛出IOException 时,也必须调用此方法关闭源。
CacheDataSink
CacheDataSink是DataSink主要实现,主要目的是将数据写入文件缓存,通过Cache获取文件打开写入文件,当达到指定分段大小就获取下个文件继续写入,可以设置数据分段的长度和写入缓冲区的大小。
public static final long DEFAULT_FRAGMENT_SIZE = 5 * 1024 * 1024;
public static final int DEFAULT_BUFFER_SIZE = 20 * 1024;
@Override
public void open(DataSpec dataSpec) throws CacheDataSinkException {
...
try {
openNextOutputStream(dataSpec);
} catch (IOException e) {
throw new CacheDataSinkException(e);
}
}
private void openNextOutputStream(DataSpec dataSpec) throws IOException {
long length =
dataSpec.length == C.LENGTH_UNSET
? C.LENGTH_UNSET
: min(dataSpec.length - dataSpecBytesWritten, dataSpecFragmentSize);
file =
cache.startFile(
castNonNull(dataSpec.key), dataSpec.position + dataSpecBytesWritten, length);
FileOutputStream underlyingFileOutputStream = new FileOutputStream(file);
if (bufferSize > 0) {
if (bufferedOutputStream == null) {
bufferedOutputStream =
new ReusableBufferedOutputStream(underlyingFileOutputStream, bufferSize);
} else {
bufferedOutputStream.reset(underlyingFileOutputStream);
}
outputStream = bufferedOutputStream;
} else {
outputStream = underlyingFileOutputStream;
}
outputStreamBytesWritten = 0;
}
@Override
public void write(byte[] buffer, int offset, int length) throws CacheDataSinkException {
@Nullable DataSpec dataSpec = this.dataSpec;
if (dataSpec == null) {
return;
}
try {
int bytesWritten = 0;
while (bytesWritten < length) {
if (outputStreamBytesWritten == dataSpecFragmentSize) {
closeCurrentOutputStream();
openNextOutputStream(dataSpec);
}
int bytesToWrite =
(int) min(length - bytesWritten, dataSpecFragmentSize - outputStreamBytesWritten);
castNonNull(outputStream).write(buffer, offset + bytesWritten, bytesToWrite);
bytesWritten += bytesToWrite;
outputStreamBytesWritten += bytesToWrite;
dataSpecBytesWritten += bytesToWrite;
}
} catch (IOException e) {
throw new CacheDataSinkException(e);
}
}
private void openNextOutputStream(DataSpec dataSpec) throws IOException {
long length =
dataSpec.length == C.LENGTH_UNSET
? C.LENGTH_UNSET
: min(dataSpec.length - dataSpecBytesWritten, dataSpecFragmentSize);
file =
cache.startFile(
castNonNull(dataSpec.key), dataSpec.position + dataSpecBytesWritten, length);
FileOutputStream underlyingFileOutputStream = new FileOutputStream(file);
if (bufferSize > 0) {
if (bufferedOutputStream == null) {
bufferedOutputStream =
new ReusableBufferedOutputStream(underlyingFileOutputStream, bufferSize);
} else {
bufferedOutputStream.reset(underlyingFileOutputStream);
}
outputStream = bufferedOutputStream;
} else {
outputStream = underlyingFileOutputStream;
}
outputStreamBytesWritten = 0;
}
private void closeCurrentOutputStream() throws IOException {
if (outputStream == null) {
return;
}
boolean success = false;
try {
outputStream.flush();
success = true;
} finally {
Util.closeQuietly(outputStream);
outputStream = null;
File fileToCommit = castNonNull(file);
file = null;
if (success) {
cache.commitFile(fileToCommit, outputStreamBytesWritten);
} else {
fileToCommit.delete();
}
}
}
可以看到CacheDataSink主要作用是控制文件分段写入,至于文件是如何获取的则交给Cache实现。
CacheEvictor
主要用来删除缓存的CacheSpan,根据实现的移除策略调用CacheSpan.removeSpan。
这个直接看实现
LeastRecentlyUsedCacheEvictor
当缓存达到设定的最大值有限,会将最近最少使用的CacheSpan删除。
public LeastRecentlyUsedCacheEvictor(long maxBytes) {
this.maxBytes = maxBytes;
this.leastRecentlyUsed = new TreeSet<>(LeastRecentlyUsedCacheEvictor::compare);
}
private static int compare(CacheSpan lhs, CacheSpan rhs) {
long lastTouchTimestampDelta = lhs.lastTouchTimestamp - rhs.lastTouchTimestamp;
if (lastTouchTimestampDelta == 0) {
return lhs.compareTo(rhs);
}
return lhs.lastTouchTimestamp < rhs.lastTouchTimestamp ? -1 : 1;
}
@Override
public void onSpanAdded(Cache cache, CacheSpan span) {
leastRecentlyUsed.add(span);
currentSize += span.length;
evictCache(cache, 0);
}
private void evictCache(Cache cache, long requiredSpace) {
while (currentSize + requiredSpace > maxBytes && !leastRecentlyUsed.isEmpty()) {
cache.removeSpan(leastRecentlyUsed.first());
}
}
可以看出CacheEvictor很简单,主要作用就是管理CacheSpan,决定哪个CacheSpan优先被移除。
CachedContentIndex
主要用于保存缓存资源的索引信息,其中包含了多个资源的信息,通过资源key查询CachedContentIndex获取到CachedContent,CachedContent又包含很多的CacheSpan,最后通过position、length查询到指定的CacheSpan。
public CachedContentIndex(
@Nullable DatabaseProvider databaseProvider,
@Nullable File legacyStorageDir,
@Nullable byte[] legacyStorageSecretKey,
boolean legacyStorageEncrypt,
boolean preferLegacyStorage) {
checkState(databaseProvider != null || legacyStorageDir != null);
keyToContent = new HashMap<>();
idToKey = new SparseArray<>();
removedIds = new SparseBooleanArray();
newIds = new SparseBooleanArray();
@Nullable
Storage databaseStorage =
databaseProvider != null ? new DatabaseStorage(databaseProvider) : null;
@Nullable
Storage legacyStorage =
legacyStorageDir != null
? new LegacyStorage(
new File(legacyStorageDir, FILE_NAME_ATOMIC),
legacyStorageSecretKey,
legacyStorageEncrypt)
: null;
if (databaseStorage == null || (legacyStorage != null && preferLegacyStorage)) {
storage = castNonNull(legacyStorage);
previousStorage = databaseStorage;
} else {
storage = databaseStorage;
previousStorage = legacyStorage;
}
}
@WorkerThread
public void initialize(long uid) throws IOException {
storage.initialize(uid);
if (previousStorage != null) {
previousStorage.initialize(uid);
}
if (!storage.exists() && previousStorage != null && previousStorage.exists()) {
previousStorage.load(keyToContent, idToKey);
storage.storeFully(keyToContent);
} else {
storage.load(keyToContent, idToKey);
}
if (previousStorage != null) {
previousStorage.delete();
previousStorage = null;
}
}
@WorkerThread
public void store() throws IOException {
storage.storeIncremental(keyToContent);
int removedIdCount = removedIds.size();
for (int i = 0; i < removedIdCount; i++) {
idToKey.remove(removedIds.keyAt(i));
}
removedIds.clear();
newIds.clear();
}
public CachedContent getOrAdd(String key) {
@Nullable CachedContent cachedContent = keyToContent.get(key);
return cachedContent == null ? addNew(key) : cachedContent;
}
private CachedContent addNew(String key) {
int id = getNewId(idToKey);
CachedContent cachedContent = new CachedContent(id, key);
keyToContent.put(key, cachedContent);
idToKey.put(id, key);
newIds.put(id, true);
storage.onUpdate(cachedContent);
return cachedContent;
}
public void applyContentMetadataMutations(String key, ContentMetadataMutations mutations) {
CachedContent cachedContent = getOrAdd(key);
if (cachedContent.applyMetadataMutations(mutations)) {
storage.onUpdate(cachedContent);
}
}
CachedContent 中保存了文件的Key 、Id 、CacheSpan、Meta信息,CachedContentIndex会通过storage将这些信息存储到文件或者数据库,CachedContentIndex中定义了2种存储方式DatabaseStorage和LegacyStorage,分别对应数据库存储和文件存储,这里我们以文件存储为例看下实现。
LegacyStorage
public LegacyStorage(File file, @Nullable byte[] secretKey, boolean encrypt) {
...
atomicFile = new AtomicFile(file);
}
@Override
public void load(
HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) {
checkState(!changed);
if (!readFile(content, idToKey)) {
content.clear();
idToKey.clear();
atomicFile.delete();
}
}
private boolean readFile(
HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) {
if (!atomicFile.exists()) {
return true;
}
@Nullable DataInputStream input = null;
try {
InputStream inputStream = new BufferedInputStream(atomicFile.openRead());
input = new DataInputStream(inputStream);
int version = input.readInt();
if (version < 0 || version > VERSION) {
return false;
}
int flags = input.readInt();
if ((flags & FLAG_ENCRYPTED_INDEX) != 0) {
.....
}
int count = input.readInt();
int hashCode = 0;
for (int i =