赞
踩
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
ExoPlayer架构详解与源码分析(13)——TeeDataSource和CacheDataSource
ExoPlayer架构详解与源码分析(14)——ProgressiveMediaPeriod
ExoPlayer架构详解与源码分析(15)——Renderer
上篇介绍完基本的DataSource,现在可以开始CacheDataSource和TeeDataSource了。
先看下整体结构:
上图这里假设CacheDataSource原始的上游数据是通过OkHttpDataSource从网络获取
看完上图,是不是感觉非常复杂,没关系我们可以拆解出几个独立的结构一步步了解,可以看到底层的Cache可以作为一个独立的结构,在说CacheDataSource和TeeDataSource前,先把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数据源的时候应该同步调用此方法。
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的一般执行顺序是:
继续深挖,在讲Cache实现前说下其他几个类
这是一个用来向其中写入数据的组件,概念上是和DataSource完全相反,提供了write供外部写入数据
看下主要方法:
CacheDataSink是DataSink主要实现,主要目的是将数据写入文件缓存,通过Cache获取文件打开写入文件,当达到指定分段大小就获取下个文件继续写入,可以设置数据分段的长度和写入缓冲区的大小。
/** 默认文件的最大大小为5M */ public static final long DEFAULT_FRAGMENT_SIZE = 5 * 1024 * 1024; /** 默认的写入流缓冲为20kb */ 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获取文件的路径 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(//这里调用了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) { //写入完成后,提交缓存,会将当前的分段的CacheSpan添加入索引 cache.commitFile(fileToCommit, outputStreamBytesWritten); } else { fileToCommit.delete(); } } }
可以看到CacheDataSink主要作用是控制文件分段写入,至于文件是如何获取的则交给Cache实现。
主要用来删除缓存的CacheSpan,根据实现的移除策略调用CacheSpan.removeSpan。
这个直接看实现
当缓存达到设定的最大值有限,会将最近最少使用的CacheSpan删除。
public LeastRecentlyUsedCacheEvictor(long maxBytes) { this.maxBytes = maxBytes; //将CacheSpan放入到TreeSet管理排序 this.leastRecentlyUsed = new TreeSet<>(LeastRecentlyUsedCacheEvictor::compare); } //指定排序规则 private static int compare(CacheSpan lhs, CacheSpan rhs) { //比较CacheSpan最后使用时间 long lastTouchTimestampDelta = lhs.lastTouchTimestamp - rhs.lastTouchTimestamp; if (lastTouchTimestampDelta == 0) { // Use the standard compareTo method as a tie-break. return lhs.compareTo(rhs); } return lhs.lastTouchTimestamp < rhs.lastTouchTimestamp ? -1 : 1; } @Override //CacheSpan添加后会回调 public void onSpanAdded(Cache cache, CacheSpan span) { leastRecentlyUsed.add(span); currentSize += span.length;//更新当前的总大小 evictCache(cache, 0); } private void evictCache(Cache cache, long requiredSpace) { //如果超出最大值则开始cache.removeSpan超出的CacheSpan while (currentSize + requiredSpace > maxBytes && !leastRecentlyUsed.isEmpty()) { cache.removeSpan(leastRecentlyUsed.first()); } }
可以看出CacheEvictor很简单,主要作用就是管理CacheSpan,决定哪个CacheSpan优先被移除。
主要用于保存缓存资源的索引信息,其中包含了多个资源的信息,通过资源key查询CachedContentIndex获取到CachedContent,CachedContent又包含很多的CacheSpan,最后通过position、length查询到指定的CacheSpan。
public CachedContentIndex(
@Nullable DatabaseProvider databaseProvider,
@Nullable File legacyStorageDir,
@Nullable byte[] legacyStorageSecretKey,
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。