long[] newSourceIds = new long[newCapacity];
long[] newOffsets = new long[newCapacity];
long[] newTimesUs = new long[newCapacity];
int[] newFlags = new int[newCapacity];
int[] newSizes = new int[newCapacity];
CryptoData[] newCryptoDatas = new CryptoData[newCapacity];
//将旧的数据,移入新的数组,将相对开始位置作为新数组的第一个位置
int beforeWrap = capacity - relativeFirstIndex;
System.arraycopy(offsets, relativeFirstIndex, newOffsets, 0, beforeWrap);
System.arraycopy(timesUs, relativeFirstIndex, newTimesUs, 0, beforeWrap);
System.arraycopy(flags, relativeFirstIndex, newFlags, 0, beforeWrap);
System.arraycopy(sizes, relativeFirstIndex, newSizes, 0, beforeWrap);
System.arraycopy(cryptoDatas, relativeFirstIndex, newCryptoDatas, 0, beforeWrap);
System.arraycopy(sourceIds, relativeFirstIndex, newSourceIds, 0, beforeWrap);
int afterWrap = relativeFirstIndex;
System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap);
System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap);
System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap);
System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap);
System.arraycopy(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap);
System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap);
offsets = newOffsets;
timesUs = newTimesUs;
flags = newFlags;
sizes = newSizes;
cryptoDatas = newCryptoDatas;
sourceIds = newSourceIds;
relativeFirstIndex = 0;
capacity = newCapacity;
}
}
//获取当前Info数组的相对位置,传入相对第一个位置的偏移量
private int getRelativeIndex(int offset) {
int relativeIndex = relativeFirstIndex + offset;
return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity;//环形指针
}
//获取当前写入MetaData的绝对位置
public final int getWriteIndex() {
return absoluteFirstIndex + length;//等于当前绝开始位置+有效的长度
}
//输入Format
@Override
public final void format(Format format) {
Format adjustedUpstreamFormat = getAdjustedUpstreamFormat(format);
upstreamFormatAdjustmentRequired = false;
unadjustedUpstreamFormat = format;
boolean upstreamFormatChanged = setUpstreamFormat(adjustedUpstreamFormat);
if (upstreamFormatChangeListener != null && upstreamFormatChanged) {
upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedUpstreamFormat);
}
}
Metadata输入主要分3部分:
1. 确保当前为关键帧。
2. 获取Info环形数组的下一个索引,将当前Sampledata的数据保存到Info数组并记录Meta信息。
3. 确保Info数组足够大可以容纳足够多的数据。下面看下sampleData部分
@Override
public final void sampleData(
ParsableByteArray data, int length, @SampleDataPart int sampleDataPart) {
sampleDataQueue.sampleData(data, length);
}
没了就这么多。,你只管告诉sampleDataQueue数据的大小和长度,sampleDataQueue来添加,具体sampleDataQueue是怎么有效管理数据的后面会讲到,现在不是重点
通过sampleDataQueue和sampleMetadata对比你会发现sampleMetadata比sampleDataQueue复杂多个,而且sampleMetadata方法添加了synchronized 同步块,多线程的时候会阻塞,而sampleDataQueue没有任何同步代码包括到sampleDataQueue里也一样。这样做是因为sampleMetadata的数据量很少,即使阻塞也能很高效的执行。而sampleData数据量往往比较大,写入的时间也比较长,所以不能阻塞。那么为什么要这么做呢,后面我们看到数据的读取部分就能理解了。
* **输出** SampleQueue提供了**read**方法输出数据
分析下对应源码:
public int read(
FormatHolder formatHolder,
DecoderInputBuffer buffer,
@ReadFlags int readFlags,
boolean loadingFinished) {
//首先读取Metadata
int result =
peekSampleMetadata(
formatHolder,
buffer,
/* formatRequired= */ (readFlags & FLAG_REQUIRE_FORMAT) != 0,
loadingFinished,
extrasHolder);
if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream()) {
boolean peek = (readFlags & FLAG_PEEK) != 0;
if ((readFlags & FLAG_OMIT_SAMPLE_DATA) == 0) {
if (peek) {
sampleDataQueue.peekToBuffer(buffer, extrasHolder);
} else {
sampleDataQueue.readToBuffer(buffer, extrasHolder);//将sampleData的位置信息通过extrasHolder传递给sampleDataQueue读取数据
}
}
if (!peek) {
readPosition++;//读取位置++
}
}
return result;
}
//读取Metadata
private synchronized int peekSampleMetadata(
FormatHolder formatHolder,
DecoderInputBuffer buffer,
boolean formatRequired,
boolean loadingFinished,
SampleExtrasHolder extrasHolder) {
…
Format format = sharedSampleMetadata.get(getReadIndex()).format; //用绝对开始位置+读取位置(absoluteFirstIndex + readPosition)获取读取的绝对位置
…
int relativeReadIndex = getRelativeIndex(readPosition);//获取当前Info数组的相对位置
...
extrasHolder.size = sizes[relativeReadIndex];//取出Info数据
extrasHolder.offset = offsets[relativeReadIndex];
extrasHolder.cryptoData = cryptoDatas[relativeReadIndex];
return C.RESULT\_BUFFER\_READ;
}
//获取读取的绝对位置
public final int getReadIndex() {
return absoluteFirstIndex + readPosition;
}
要读取sampleData和sampleMetadata的数据,首先要确定当前读取点的Info的位置,然后通过Info数组才能知道读取的位置和长度,最后读取sampleData,同样sampleMetadata读取是加锁的,而sampleData没有。可以看出sampleData的读写是不受线程限制的,通过SampleQueue内部维护的Info数组来维护sampleData,可以最大化保证多线程下sampleData读写的效率。
* **释放** 通过discardSamples等释放不需要的数据
分析下对应源码:
public final void discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) {
sampleDataQueue.discardDownstreamTo(
discardSampleMetadataTo(timeUs, toKeyframe, stopAtReadPosition));//先释放Metadata
}
private synchronized long discardSampleMetadataTo(
long timeUs, boolean toKeyframe, boolean stopAtReadPosition) {
if (length == 0 || timeUs < timesUs[relativeFirstIndex]) {
return C.INDEX_UNSET;
}
int searchLength = stopAtReadPosition && readPosition != length ? readPosition + 1 : length;
//根据时间戳来确定要释放数据块的数量
int discardCount = findSampleBefore(relativeFirstIndex, searchLength, timeUs, toKeyframe);
if (discardCount == -1) {
return C.INDEX_UNSET;
}
return discardSamples(discardCount);
}
private long discardSamples(int discardCount) {
largestDiscardedTimestampUs =
max(largestDiscardedTimestampUs, getLargestTimestamp(discardCount));
length -= discardCount;//有效长度=有效长度-释放数量
absoluteFirstIndex += discardCount;//绝对开始位置后移
relativeFirstIndex += discardCount;//相对开始位置后移
if (relativeFirstIndex >= capacity) {//环形数组
relativeFirstIndex -= capacity;
}
readPosition -= discardCount;//因为relativeFirstIndex后移,相对它的位置在缩小
if (readPosition < 0) {
readPosition = 0;
}
sharedSampleMetadata.discardTo(absoluteFirstIndex);//释放Metadata
if (length == 0) {
int relativeLastDiscardIndex = (relativeFirstIndex == 0 ? capacity : relativeFirstIndex) - 1;
return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex];
} else {
return offsets[relativeFirstIndex];//返回SampleData的释放偏移量
}
}
同样数据的释放和读写一样,通过内部的Info来管理,释放后会更新Info上的相关位置。
## SampleQueue动态分析
上面一直在说环形数组,静态的看代码可能感受不到这点,下面我们通过几个图来动态分析下SampleQueue的运作原理,这部分需要结合上面的源码一起看才好理解。
首先为了方便分析假设数组最大长度为capacity=6,当前已经写入了4段sampleData对应图中的sample0-3,同时也写入了4个数据到Info数组对应图中的0-2的size、offset、time结构,每个结构的size、offset、time在图的最下方都有标记,这里可以看下加深下对这个结构数值的含义的理解,所以此时有效长度length=4,由于是刚开始读写这个时候的relativeFirstIndex=absoluteFirstIndex=0处于开始位置,当前的读取位置相对于relativeFirstIndex也就是和relativeFirstIndex差值,readPosition=2。接下来sample会从右侧箭头处不断写入。同样上方的Info数组随着读取也会不断变化。
![在这里插入图片描述](https://img-blog.csdnimg.cn/6c912a25b7a441579832978253d1d24d.png)
好了,此时先向Sample队列写入一个Sample length+1,然后同时读取2个Sample readPosition+2=4
![在这里插入图片描述](https://img-blog.csdnimg.cn/fbe48004e98a4f7e8aaf31b53e75d8db.png)
随着数据被使用(已经播放)之前的数据需要丢弃,以便下次写入,释放3个Sample,可以看到relativeFirstIndex和absoluteFirstIndex同时前移,虽然readPosiition的位置没有移动,也就是没有读取新的数据,但是readPosiition的值变小,有效长度缩小为2。到这里relativeFirstIndex还是和absoluteFirstIndex相等的还看不出环形的特性。
![在这里插入图片描述](https://img-blog.csdnimg.cn/9b9338b00acc4b6494c5cebaf5361fa8.png)
此时开始写入3个Sample,这个时候就可以看出Info环形特性,之前释放的0号和1号会重新指向数据队列的最前端,同时更新offset,size相关数据,有效数据长度增加到5,length=5。
![在这里插入图片描述](https://img-blog.csdnimg.cn/ac1a0630bb0a4e7bbed2b4a5ff883642.png)
这个时候再读取3个Sample时,readPosiition的值增加3此时指向Info数组的下标1,readPosiition=4。
![在这里插入图片描述](https://img-blog.csdnimg.cn/b596def3e23149ceb0eea6215171cd75.png)
好了我们继续释放数据,这次再次释放3个,可以看到relativeFirstIndex和absoluteFirstIndex值开始不一样了,由于又回到了Info数组的开始位置所以relativeFirstIndex=0,readPosiition缩短为1,有效长度length=2,而absoluteFirstIndex是相对于Smaple的绝对位置,这个时候absoluteFirstIndex继续后移到6号位置的sample6。
![在这里插入图片描述](https://img-blog.csdnimg.cn/1af52e1e804f4686bae4f92fd32fc7ea.png)
把上面的图连续不断的执行,可以想象出,Info数组像一个不断前行的履带行驶在sample铺平的道路上(读取)。被履带压过的道路(已经读取过的数据)就会被释放,此时路还在不断的向前铺设(新的sample数据在不断的写入到SampleQueen中),整个过程中如果新增数据比释放数据快,履带的大小会动态的扩充变长,图中为了方便理解并没有体现这点。
至此我们可以总结出几个规律:
* 当前写入数据的绝对位置永远等于absoluteFirstIndex+length
* 当前读取数据的绝对位置永远等于absoluteFirstIndex + readPosition
* Info数组可以理解成一个首尾相连的环形数组,数组最后一个位置的下个位置就是数组的开始位置
这几个规律在源码中经常被用到,感兴趣的同学可以深入阅读下。
下面来分析下实际存储数据的2个结构
## SpannedData< SharedSampleMetadata >
本质是一个Android里实现SparseArray的map,通过int 类型key可以快速向指定key存入数据或者取出数据,这里数据跟随SampleQueue里的Info来管理,添加或者释放指定位置的Metadata数据。
## SampleDataQueue
重点来说下SampleDataQueue,由于SampleData的数据量要远远大于Metadata,而且还需要频繁的读写释放,所以向SpannedData那样简单粗暴的管理数据,效率会非常低,因为JVM要频繁的申请内存GC释放内存。为了解决这个问题SampleDataQueue内部维护了一个链表,同时维护了链表中3个重要的节点,firstAllocationNode,readAllocationNode,writeAllocationNode,用于快速获取读写点。
private AllocationNode firstAllocationNode;//第一个节点位置
private AllocationNode readAllocationNode;//当前读取节点位置
private AllocationNode writeAllocationNode;//当前写入节点位置
这里顺带提下AllocationNode的数据结构,主要就是封装了Allocation,allocation才是实际存储数据的部分,同时提供了next AllocationNode 提供下一个AllocationNode 的指针,形成一个链表结构。
private static final class AllocationNode implements Allocator.AllocationNode {
public long startPosition;//此段allocation的开始位置
public long endPosition;//此段allocation的结束位置
public Allocation allocation;//实际缓存数据部分
public AllocationNode next;//指向下一个
…
public void initialize(Allocation allocation, AllocationNode next) {
this.allocation = allocation;
this.next = next;
}
…
回到SampleDataQueue中,调用sampleData循环写入数据时,每次循环写入主要分为3步:
1. 调用preAppend初始化通过writeAllocationNode,同时也初始化出了下个writeAllocationNode。重点看下allocator.allocate(),这里才是初始化了AllocationNode的实际内存数据allocation的地方,后面会详细分析。
2. 将数据循环写入writeAllocationNode的实际缓存位置allocation.data中。
3. 调用preAppend更新SampleDataQueue已写入总长度,如果总长度已经超过当前写入节点的结束位置,将当前写入节点,更新为下一写入节点。
private int preAppend(int length) {
if (writeAllocationNode.allocation == null) {
writeAllocationNode.initialize(
allocator.allocate(),//分配内存
new AllocationNode(writeAllocationNode.endPosition, allocationLength));
}
return min(length, (int) (writeAllocationNode.endPosition - totalBytesWritten));
}
public void sampleData(ParsableByteArray buffer, int length) {
while (length > 0) {
int bytesAppended = preAppend(length);
buffer.readBytes(
writeAllocationNode.allocation.data,
writeAllocationNode.translateOffset(totalBytesWritten),
bytesAppended);
length -= bytesAppended;
postAppend(bytesAppended);
}
}
private void postAppend(int length) {
totalBytesWritten += length;//增加总长度
if (totalBytesWritten == writeAllocationNode.endPosition) {//总长度已经超过当前写入节点的结束位置
writeAllocationNode = writeAllocationNode.next;//将当前写入节点,更新为下一写入节点
}
}
读取和写入类似,直接看下释放数据的地方,首先释放指定位置之前的链表数据,其次重置开始节点和读取节点。
public void discardDownstreamTo(long absolutePosition) {
if (absolutePosition == C.INDEX_UNSET) {
return;
}
while (absolutePosition >= firstAllocationNode.endPosition) {
//从第一个节点开始依次取出下一个节点通过allocator释放内存,并清除AllocationNode,一直到指定的absolutePosition
allocator.release(firstAllocationNode.allocation);
firstAllocationNode = firstAllocationNode.clear();
}
if (readAllocationNode.startPosition < firstAllocationNode.startPosition) {
//保证当前的读取位置在开始节点之后
readAllocationNode = firstAllocationNode;
}
}
看完是不是发现目前也没没有解决上面说的内存问题,内存感觉是在不断新增的。注意看下上面源码实际获取内存的地方allocator.allocate(),原来这些都交给了Allocator,通过Allocator实现内存的循环高效利用。
## Allocator
这是一个接口用于媒体数据的内存分配,默认有一个DefaultAllocator实现。
先看下主要的源码
public final class DefaultAllocator implements Allocator {
private static final int AVAILABLE_EXTRA_CAPACITY = 100;//额外的初始化Allocation数量
private final boolean trimOnReset;
private final int individualAllocationSize;
@Nullable private final byte[] initialAllocationBlock;//初始化的一个连续的数组,指向默认数量的的Allocations,参考下图
private int targetBufferSize;
private int allocatedCount;//已分配的Allocation数量,参考下图
private int availableCount;//可用的Allocation数量,参考下图
private @NullableType Allocation[] availableAllocations;//可用的Allocations,参考下图
public DefaultAllocator(
boolean trimOnReset, int individualAllocationSize, int initialAllocationCount) {
this.trimOnReset = trimOnReset;
this.individualAllocationSize = individualAllocationSize;
this.availableCount = initialAllocationCount;
this.availableAllocations = new Allocation[initialAllocationCount + AVAILABLE_EXTRA_CAPACITY];//添加了部分冗余
if (initialAllocationCount > 0) {//将初始化的Allocations通过指定offset分配initialAllocationBlock
initialAllocationBlock = new byte[initialAllocationCount * individualAllocationSize];
for (int i = 0; i < initialAllocationCount; i++) {
int allocationOffset = i * individualAllocationSize;
availableAllocations[i] = new Allocation(initialAllocationBlock, allocationOffset);
}
} else {
initialAllocationBlock = null;
}
}
//这个方法用于获取一个Allocation,注意在调用此方法后必须调用release方法将分配Allocation返还
@Override
public synchronized Allocation allocate() {
allocatedCount++;//已分配数量+1
Allocation allocation;
if (availableCount > 0) {
allocation = Assertions.checkNotNull(availableAllocations[–availableCount]);//从尾部取出,可用数量-1
availableAllocations[availableCount] = null;//清空
} else {
allocation = new Allocation(new byte[individualAllocationSize], 0);//不够用了,创建新的Allocation,直接初始化出一段新的数组分配给它
if (allocatedCount > availableAllocations.length) {//可用Allocations扩充2倍
availableAllocations = Arrays.copyOf(availableAllocations, availableAllocations.length * 2);
}
}
return allocation;
}
//返还分配的Allocation
@Override
public synchronized void release(Allocation allocation) {
availableAllocations[availableCount++] = allocation;//可用数量加1
allocatedCount–;//已分配数量减1
}
…
}
@Override//释无用的空块
public synchronized void trim() {
//如果重新定义了缓存区大小,计算需要的Allocation块总数量
int targetAllocationCount = Util.ceilDivide(targetBufferSize, individualAllocationSize);
int targetAvailableCount = max(0, targetAllocationCount - allocatedCount);//减去目前空余的块,则为剩余需要的块数量
//不存在冗余,无需trim
if (targetAvailableCount >= availableCount) {
return;
}
if (initialAllocationBlock != null) {
// 从头尾查找第一个不是空的块,将其位置向前
int lowIndex = 0;
int highIndex = availableCount - 1;
while (lowIndex <= highIndex) {
//未分配出的Allocation 不可能为null
Allocation lowAllocation = Assertions.checkNotNull(availableAllocations[lowIndex]);
if (lowAllocation.data == initialAllocationBlock) {//当前低位为初始值,从未被分配过
lowIndex++;//lowIndex后移
} else {//当前低位Allocation已分配过
//未分配出的Allocation 不可能为null
Allocation highAllocation = Assertions.checkNotNull(availableAllocations[highIndex]);
if (highAllocation.data != initialAllocationBlock) {//当前高位Allocation已分配过
highIndex--;//highIndex前移
} else {//当前高位Allocation未分配过
//将当前未分配过高位和已分配过的低位交换位置,未分配过的放到数组低位
availableAllocations[lowIndex++] = highAllocation;
availableAllocations[highIndex--] = lowAllocation;
最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!
给大家整理的视频资料:
给大家整理的电子书资料:
如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以点击这里获取!
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
书、PPT等共享给大家!
给大家整理的视频资料:
[外链图片转存中…(img-WmJY7qbX-1714245495225)]
给大家整理的电子书资料:
[外链图片转存中…(img-S7kzEnZ2-1714245495225)]
如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以点击这里获取!
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!