FFmpeg 内存模型分析
通过前面的文章我们知道,AVPacket和AVFrame是储存着音视频解码前数据和解码后数据的重要结构体,我们使用av_read_frame()函数将解封装后的数据存入每个AVPacket,使用avcodec_receive_frame()函数将解码后的数据存入每个AVFrame,这时不可避免会出现储存空间的分配与释放问题,高明的FFmpeg是如何设计并解决这一问题的呢?
先让我们一下AVPacket和AVFrame:
一、struct AVPacket
typedef struct AVPacket {
//指向引用计数缓冲区的引用
AVBufferRef *buf;
//显示时间戳
int64_t pts;
//解码时间戳
int64_t dts;
//压缩编码的数据
uint8_t *data;
//data的大小
int size;
//标识该AVPacket所属的视频/音频流。
int stream_index;
/**
* A combination of AV_PKT_FLAG values
*/
int flags;
/**
* Additional packet data that can be provided by the container.
* Packet can contain several types of side information.
*/
AVPacketSideData *side_data;
int side_data_elems;
/**
* Duration of this packet in AVStream->time_base units, 0 if unknown.
* Equals next_pts - this_pts in presentation order.
*/
int64_t duration;
int64_t pos; ///< byte position in stream, -1 if unknown
#if FF_API_CONVERGENCE_DURATION
/**
* @deprecated Same as the duration field, but as int64_t. This was required
* for Matroska subtitles, whose duration values could overflow when the
* duration field was still an int.
*/
attribute_deprecated
int64_t convergence_duration;
#endif
} AVPacket;
以上是FFmpeg avcodec.h文件中关于AVPacket的源码,我们可以提炼出以下关键:
- 此结构存储被压缩的数据(关键数据见源码注释)。它通常由解复用器导出然后作为输入传递给解码器,或者从编码器输出并传入复用器。
- 对于视频,它通常应该包含一个压缩帧。对于音频,它可以包含多个压缩帧。
- 使用AVBufferRef指向一个引用计数缓冲区。
二、struct AVFrame
这篇文章中我们的任务是去探究FFmpeg的内存结构,所以我们不去深究AVFrame中究竟封装了哪些具体数据元素。
在AVFrame的源码中我们同样发现,他也使用AVBufferRef指向一个引用计数缓冲区。
三、struct AVBufferRef
/**
* A reference to a data buffer.
*
* The size of this struct is not a part of the public ABI and it is not meant
* to be allocated directly.
*/
typedef struct AVBufferRef {
AVBuffer *buffer;
/**
* The data buffer. It is considered writable if and only if
* this is the only reference to the buffer, in which case
* av_buffer_is_writable() returns 1.
*/
uint8_t *data;
/**
* Size of data in bytes.
*/
int size;
} AVBufferRef;
AVBufferRef是单个对数据缓冲区AVBuffer的引用,他是AVPacket、AVFrame和数据缓冲区AVBuffer之间的桥梁。下面我们就正式揭开上文提到的“引用数据缓冲区”的面纱。
四、struct AVBuffer
AVBuffer是FFmpeg中很常用的一种缓冲区,缓冲区使用引用计数(reference-counted)机制。
-
这个API中有两个核心对象——AVBuffer和AVBufferRef。AVBuffer 表示数据缓冲区本身;它是不透明的(非常类似与private),不应被访问或由调用方直接调用,只能通过AVBufferRef访问。
-
但是,调用者可以通过比较两个AVBuffer指针,检查两个不同引用是否指向同一数据缓冲区。
-
AVBufferRef表示单个对AVBuffer的引用,它是一个可以由调用者直接调用的对象。
-
有两个函数用于创建新的AVBuffer :
- av_buffer_alloc()
AVBufferRef *av_buffer_alloc(int size)
{
AVBufferRef *ret = NULL;
uint8_t *data = NULL;
data = av_malloc(size);
if (!data)
return NULL;
ret = av_buffer_create(data, size, av_buffer_default_free, NULL, 0);
if (!ret)
av_freep(&data);
return ret;
}
使用av_malloc分配一个新的缓冲区
调用av_buffer_create()创建 AVBufferRef::*buffer成员,用于管理AVBuffer缓冲区
- av_buffer_create()
函数主要功能就是初始化AVBuffer AVBufferRef::*buffer成员,即为ref->buffer各字段赋值,最终,AVBufferRef *ref全部构造完毕,将之返回。
AVBufferRef *av_buffer_create(uint8_t *data, int size,
void (*free)(void *opaque, uint8_t *data),
void *opaque, int flags)
{
AVBufferRef *ref = NULL;
AVBuffer *buf = NULL;
buf = av_mallocz(sizeof(*buf));
if (!buf)
return NULL;
buf->data = data;
buf->size = size;
buf->free = free ? free : av_buffer_default_free;
buf->opaque = opaque;
atomic_init(&buf->refcount, 1);
if (flags & AV_BUFFER_FLAG_READONLY)
buf->flags |= BUFFER_FLAG_READONLY;
ref = av_mallocz(sizeof(*ref));
if (!ref) {
av_freep(&buf);
return NULL;
}
ref->buffer = buf;
ref->data = data;
ref->size = size;
return ref;
}
- 可以使用 av_buffer_ref() 从现有引用,创建新引用。
AVBufferRef *av_buffer_ref(AVBufferRef *buf)
{
AVBufferRef *ret = av_mallocz(sizeof(*ret));
if (!ret)
return NULL;
*ret = *buf;
atomic_fetch_add_explicit(&buf->buffer->refcount, 1, memory_order_relaxed);
return ret;
}
a) *ret = *buf; 这充分说明了新引用将与现有引用共用一份缓冲区。
b) atomic_fetch_add_explicit();当拷贝发生时,将AVBuffer缓冲区引用计数加1。
- 使用av_buffer_unref() 释放引用(这将在释放所有引用的同时释放所有数据)。
static void buffer_replace(AVBufferRef **dst, AVBufferRef **src)
{
AVBuffer *b;
b = (*dst)->buffer;
if (src) {
**dst = **src;
av_freep(src);
} else
av_freep(dst);
if (atomic_fetch_add_explicit(&b->refcount, -1, memory_order_acq_rel) == 1) {
b->free(b->opaque, b->data);
av_freep(&b);
}
}
void av_buffer_unref(AVBufferRef **buf)
{
if (!buf || !*buf)
return;
buffer_replace(buf, NULL);
}
a) 回收AVBufferRef **buf内存
b) AVBuffer的引用计数减1,若引用计数为0,则通过b->free(b->opaque, b->data);调用回调函数回收AVBuffer缓冲区内存。
- 由此看出,等效在AVPacket中的操作就是:
五、总结
1.浅拷贝模式:
由图容易看出两个avpacket指向同一AVBuffer空间。
2.引用计数机制:一种垃圾回收机制,对于一个内存块,除内存块本身外增加一个引用计数,每当这个内存块被外部的指针或内存块所引用的时候,引用计数加1;当引用它的指针释放了对它的引用的时候,则引用计数减1,同时检查引用计数的值,当引用计数为0时,就销毁该内存块并释放其所占用的内存。引用计数机制需要2个函数,一个是增加引用计数,一个是减少引用计数并当计数减少到0时释放内存。