系列文章目录
FFmpeg源码解析系列(一)目录和编译
FFmpeg源码解析系列(二)主要结构体
ffmpeg源码解析系列(四)结构体之AVIOContext 、URLContext、URLProtocol
ffmpeg源码解析系列(五)结构体之AVCodecContext
这是毕业一年跳槽时被面过的一道题,他问ffmpeg的内存管理是怎样的。当时支支吾吾没有回答出来。
如果再让我回到过去,我会和他说莫欺少年穷~
一、ffmpeg 内存管理相关api
1.1. av_malloc
和 av_free
FFmpeg 提供了自定义的内存分配函数 av_malloc
和释放函数 av_free
,它们封装了系统的内存分配接口,并提供了一些额外的内存检查功能。
-
av_malloc(size_t size): 用于分配指定大小的内存块。如果分配失败,会返回
NULL
。 -
av_free(void *ptr): 释放由
av_malloc
分配的内存块。
使用示例,
void *buffer = av_malloc(1024);
if (!buffer) {
fprintf(stderr, "Memory allocation failed\n");
return;
}
// 使用 buffer
av_free(buffer);
1.2. AVBuffer
和 AVBufferRef
AVBuffer
是 FFmpeg 中引用计数的核心结构,用于管理数据缓冲区的生命周期。AVBufferRef
是 AVBuffer
的引用,使用引用计数机制来确保内存被正确释放。
主要API函数:
-
av_buffer_alloc(size_t size): 分配一个指定大小的缓冲区。
-
AVBufferRef *av_buffer_create(uint8_t *data, size_t size, void (*free)(void *opaque, uint8_t *data),void *opaque, int flags); 已申请的数据使用AVBufferRef来管理
-
av_buffer_ref(AVBufferRef *buf): 增加缓冲区的引用计数。
-
av_buffer_unref(AVBufferRef buf): 释放一个缓冲区的引用,当引用计数为0时,释放缓冲区。
以下是一个简单例子
#include <libavutil/buffer.h>
#include <stdio.h>
// 自定义释放函数
void custom_free(void *opaque, uint8_t *data) {
printf("Freeing buffer: %p\n", data);
// 如果有其他的释放逻辑,比如释放 opaque 指针,需要在这里进行
av_free(data);
}
int main() {
// 1. 使用 av_buffer_alloc 分配一个指定大小的缓冲区
size_t size = 1024; // 缓冲区大小
AVBufferRef *buf1 = av_buffer_alloc(size);
if (!buf1) {
fprintf(stderr, "Failed to allocate buffer\n");
return -1;
}
printf("Buffer allocated with size: %zu\n", size);
// 2. 使用 av_buffer_create 管理已申请的缓冲区
uint8_t *data = av_malloc(size); // 手动分配数据
if (!data) {
fprintf(stderr, "Failed to manually allocate data\n");
av_buffer_unref(&buf1);
return -1;
}
// 使用自定义释放函数来管理 data
AVBufferRef *buf2 = av_buffer_create(data, size, custom_free, NULL, 0);
if (!buf2) {
fprintf(stderr, "Failed to create buffer with custom free\n");
av_free(data); // 如果失败需要手动释放 data
av_buffer_unref(&buf1);
return -1;
}
printf("Buffer created with custom free function\n");
// 3. 使用 av_buffer_ref 增加缓冲区的引用计数
AVBufferRef *buf_ref = av_buffer_ref(buf2);
if (!buf_ref) {
fprintf(stderr, "Failed to reference buffer\n");
av_buffer_unref(&buf2);
av_buffer_unref(&buf1);
return -1;
}
printf("Buffer reference count increased\n");
// 4. 释放缓冲区的引用,引用计数减到 0 时,缓冲区被释放
av_buffer_unref(&buf_ref);
printf("Buffer reference count decreased\n");
// 释放原始缓冲区
av_buffer_unref(&buf2);
av_buffer_unref(&buf1);
printf("Buffers released\n");
return 0;
}
1.3 AVPacket 和 AVFrame
AVPacket
和 AVFrame
是 FFmpeg 中用于处理音视频数据的核心结构。AVPacket
通常用于存储编码的数据包(例如压缩的音视频数据),而 AVFrame
用于存储解码后的原始音视频数据。
下面是一些常用的内存管理相关 API 及其示例,包括如何分配、引用计数管理和释放 AVPacket
和 AVFrame
。
-
av_packet_alloc
和av_frame_alloc
:用于分配AVPacket
和AVFrame
结构。 -
av_new_packet
:为AVPacket
分配指定大小的数据缓冲区。 -
av_frame_get_buffer
:为AVFrame
分配缓冲区,用于存储图像或音频数据。 -
av_packet_ref
和av_frame_ref
:增加引用计数,使多个指针可以安全地共享相同的数据。 -
av_packet_unref
和av_frame_unref
:减少引用计数,当引用计数为 0 时,释放数据缓冲区。 -
av_packet_free
和av_frame_free
:释放AVPacket
和AVFrame
结构本身。
以下是AVPacket的一个简单例子
#include <libavcodec/avcodec.h>
#include <stdio.h>
int main() {
// 1. 分配一个 AVPacket
AVPacket *pkt = av_packet_alloc();
if (!pkt) {
fprintf(stderr, "Failed to allocate AVPacket\n");
return -1;
}
printf("AVPacket allocated\n");
// 2. 为 AVPacket 分配数据
int size = 1024;
if (av_new_packet(pkt, size) < 0) {
fprintf(stderr, "Failed to allocate data for AVPacket\n");
av_packet_free(&pkt);
return -1;
}
printf("AVPacket data allocated with size: %d\n", size);
// 模拟填充数据
for (int i = 0; i < size; i++) {
pkt->data[i] = i % 256;
}
// 3. 增加引用计数,pkt_ref 和pkt共享一份数据
AVPacket *pkt_ref = av_packet_alloc();
if (!pkt_ref || av_packet_ref(pkt_ref, pkt) < 0) {
fprintf(stderr, "Failed to reference AVPacket\n");
av_packet_free(&pkt);
av_packet_free(&pkt_ref);
return -1;
}
printf("AVPacket reference count increased\n");
// 4. 释放引用
av_packet_unref(pkt_ref);
printf("AVPacket reference count decreased\n");
// 释放原始数据
av_packet_unref(pkt);
printf("AVPacket released\n");
// 释放 AVPacket 结构本身,
av_packet_free(&pkt);
av_packet_free(&pkt_ref);
printf("AVPacket structures freed\n");
return 0;
}
以下是AVFrame的一个简单例子
#include <libavutil/frame.h>
#include <stdio.h>
int main() {
// 1. 分配一个 AVFrame
AVFrame *frame = av_frame_alloc();
if (!frame) {
fprintf(stderr, "Failed to allocate AVFrame\n");
return -1;
}
printf("AVFrame allocated\n");
// 假设要处理 YUV420P 格式的图像
frame->format = AV_PIX_FMT_YUV420P;
frame->width = 640;
frame->height = 480;
// 2. 为 AVFrame 分配缓冲区
if (av_frame_get_buffer(frame, 32) < 0) { // 32 表示对齐要求
fprintf(stderr, "Failed to allocate buffer for AVFrame\n");
av_frame_free(&frame);
return -1;
}
printf("AVFrame buffer allocated\n");
// 3. 增加引用计数
AVFrame *frame_ref = av_frame_alloc();
if (!frame_ref || av_frame_ref(frame_ref, frame) < 0) {
fprintf(stderr, "Failed to reference AVFrame\n");
av_frame_free(&frame);
av_frame_free(&frame_ref);
return -1;
}
printf("AVFrame reference count increased\n");
// 4. 释放引用
av_frame_unref(frame_ref);
printf("AVFrame reference count decreased\n");
// 释放原始帧
av_frame_unref(frame);
printf("AVFrame released\n");
// 释放 AVFrame 结构
av_frame_free(&frame);
av_frame_free(&frame_ref);
printf("AVFrame structures freed\n");
return 0;
}
二、av_malloc
和 av_free
2.1 头文件
libavutil/mem.h
2.2 av_malloc
2.2.1 功能
av_malloc
分配指定大小的内存,并根据需要对齐内存。它首先检查分配的大小是否超过了最大允许的分配大小(max_alloc_size
),然后根据平台可用的内存分配方法选择适当的对齐策略。
2.2.2 源码解析
void *av_malloc(size_t size)
{
void *ptr = NULL;
//检查请求的内存大小 size 是否超过了最大允许分配大小(max_alloc_size)。使用了 C11 标准库中的 atomic_load_explicit 函数,以确保读取是线程安全的。如果 size 超过 max_alloc_size,则函数返回 NULL。
if (size > atomic_load_explicit(&max_alloc_size, memory_order_relaxed))
return NULL;
//根据不同平台选择不同的内存对齐方式,如果没有使用malloc
...
//如果 ptr 为 NULL 且 size 为 0,这段代码将尝试分配 1 字节的内存。这是为了处理某些系统上 malloc(0) 返回 NULL 的情况。
if(!ptr && !size) {
size = 1;
ptr= av_malloc(1);
}
//在编译时,如果启用了内存填充选项(CONFIG_MEMORY_POISONING),这段代码会将分配的内存全部填充为特定的值(FF_MEMORY_POISON)。这通常用于调试,以便更容易检测未初始化或已释放的内存使用问题。
#if CONFIG_MEMORY_POISONING
if (ptr)
memset(ptr, FF_MEMORY_POISON, size);
#endif
return ptr;
}
2.3 av_free
2.3.1 功能
释放内存
2.3.2 源码解析
//正常的free函数,有内存对齐的malloc的情况需要特殊处理
void av_free(void *ptr)
{
#if HAVE_ALIGNED_MALLOC
_aligned_free(ptr);
#else
free(ptr);
#endif
}
//对 av_free 的行为进行了封装,加入了一些额外的操作来管理内存的释放和指针的更新。这个函数特别适用于那些需要在释放内存的同时将指针设为 NULL 的场景。注意这里要传入二级指针,比如你想释放ptr,需要调用av_freep(&ptr);
void av_freep(void *arg)
{
//定义一个 void * 类型的局部变量 val,用于存储 arg 指向的内存地址。
void *val;
//使用 memcpy 从 arg 指向的地址中拷贝数据到 val 中。这里拷贝的大小是 sizeof(val),即指针的大小。这样,val 就保存了 arg 指向的实际内存地址。
memcpy(&val, arg, sizeof(val));
//将 arg 指向的内存区域更新为 NULL。这通过 memcpy 实现,将 &(void *){ NULL }(一个临时的 NULL 指针)拷贝到 arg 指向的位置。此时,arg 指向的内存中的值被设置为 NULL。即如果传入&ptr,那么ptr将被置空
memcpy(arg, &(void *){ NULL }, sizeof(val));
//调用 av_free 释放 val 指向的内存块。这里的 val 是之前从 arg 拷贝过来的地址。
av_free(val);
}
三、AVBuffer和AVBufferRef
3.1 头文件
libavutil/buffer.h
3.2 结构体
3.2.1 AVBuffer结构体
AVBuffer
是 FFmpeg 内部用于表示内存缓冲区的数据结构。它包含了实际的数据指针和相关的管理信息
struct AVBuffer {
uint8_t *data;//这个buffer存储的数据
size_t size; //这个buffer存储的数据大小
atomic_uint refcount;//对该内存缓冲区的引用的数量
//释放该数据的回调,类似析构函数
void (*free)(void *opaque, uint8_t *data);
void *opaque;
int flags;
int flags_internal;
};
3.2.2 AVBufferRef结构体
AVBufferRef
是一个引用计数的包装器,用于管理 AVBuffer
的生命周期。它包含一个指向 AVBuffer
的指针和一个引用计数器。
typedef struct AVBufferRef {
AVBuffer *buffer;
uint8_t *data; //和buffer中的data内容相同
size_t size; //和buffer中的size内容相同
} AVBufferRef;
3.3 引用计数机制
对于多个指针共享同一个缓存空间,FFmpeg使用的引用计数的机制(reference-count):
初始化引用计数为0,只有真正分配AVBuffer的时候,引用计数初始化为1;
当有新的Packet引用共享的缓存空间时,就将引用计数+1;
当引用计数为0时,释放AVBuffer中的内容。
相当于通过C语言实现了C++的智能指针。如果对该机制还不太了解可以了解下C++的智能指针的相关概念。
3.4 创建AVBufferRef
av_buffer_create 先申请了一个AVBuffer,然后再创建AVBufferRef
AVBufferRef *av_buffer_create(uint8_t *data, size_t size,
void (*free)(void *opaque, uint8_t *data),
void *opaque, int flags) {
AVBufferRef *ret;
AVBuffer *buf = av_mallocz(sizeof(*buf));
if (!buf)
return NULL;
ret = buffer_create(buf, data, size, free, opaque, flags);
if (!ret) {
av_free(buf);
return NULL;
}
return ret;
}
static AVBufferRef *buffer_create(AVBuffer *buf, uint8_t *data, size_t size,
void (*free)(void *opaque, uint8_t *data),
void *opaque, int flags)
{
AVBufferRef *ref = NULL;
buf->data = data;
buf->size = size;
buf->free = free ? free : av_buffer_default_free;
buf->opaque = opaque;
atomic_init(&buf->refcount, 1);
buf->flags = flags;
ref = av_mallocz(sizeof(*ref));
if (!ref)
return NULL;
ref->buffer = buf;
ref->data = data;
ref->size = size;
return ref;
}
3.5 增加引用
AVBufferRef *av_buffer_ref(const AVBufferRef *buf)
{
AVBufferRef *ret = av_mallocz(sizeof(*ret));
if (!ret)
return NULL;
//拷贝buf中的指针
*ret = *buf;
//将AVBuffer中的引用计数+1
atomic_fetch_add_explicit(&buf->buffer->refcount, 1, memory_order_relaxed);
return ret;
}
3.6 减少引用
void av_buffer_unref(AVBufferRef **buf)
{
if (!buf || !*buf)
return;
//把buffer和null替换,那就是av_buffer_unref。
buffer_replace(buf, NULL);
}
static void buffer_replace(AVBufferRef **dst, AVBufferRef **src)
{
AVBuffer *b;
//获取 AVBuffer 指针:
b = (*dst)->buffer;
if (src) {
//内存替换
**dst = **src;
av_freep(src);
} else
//释放dst及内存
av_freep(dst);
//减少 AVBuffer 的引用计数并检查是否需要释放:atomic_fetch_sub_explicit返回的是减少之前的值
if (atomic_fetch_sub_explicit(&b->refcount, 1, memory_order_acq_rel) == 1) {
int free_avbuffer = !(b->flags_internal & BUFFER_FLAG_NO_FREE);
b->free(b->opaque, b->data);
if (free_avbuffer)
av_free(b);
}
}
四、AVPacket和AVFrame
4.1 头文件
libavcodec/packet.h
libavcodec/frame.h
4.2 依赖关系
4.3 结构体
4.3.1 AVPacket
AVPacket
结构体在 FFmpeg 中用于表示媒体数据包,包含音频或视频数据及其相关信息。结构体仅列出内存管理相关。
typedef struct AVPacket {
//如果 buf 不为空,AVPacket 使用引用计数机制来管理其数据缓冲区的生命周期。
//AVBufferRef 提供引用计数功能,当 AVPacket 不再使用时,引用计数会减少,并在引用计数为 0 时释放内存。
//如果 buf 为 NULL,则 AVPacket 的数据不使用引用计数管理。这种情况下,内存管理由 AVPacket 本身负责。
AVBufferRef *buf;
//指向实际的数据缓冲区。data 是一个指针,指向存储音频或视频数据的内存区域。
//如果 buf 不为空,data 指向的内存由 AVBufferRef 管理。data 只是 AVBufferRef 内部数据的指针,不直接负责内存管理。
//如果 buf 为空,data 指向的内存可能需要显式释放,通常在不再使用 AVPacket 时释放 data 所指向的内存。
uint8_t *data;
int size;
//提供一个 AVBufferRef,供 API 用户自由使用。FFmpeg 不会检查这个缓冲区的内容,只在 AVPacket 被取消引用时调用 av_buffer_unref() 来处理。
//用户可以使用 opaque_ref 存储附加的私有数据。
//FFmpeg 会在 AVPacket 取消引用时调用 av_buffer_unref() 来释放 opaque_ref。
//在 av_packet_copy_props() 调用时,会创建新的 AVBufferRef 引用到 opaque_ref。
AVBufferRef *opaque_ref;
} AVPacket;
4.3.2 AVFrame
AVFrame
结构体在 FFmpeg 中用于表示解码后的音频或视频帧。该结构体的内存管理机制主要涉及几个字段,包括数据缓冲区的引用计数、私有数据处理和扩展数据管理。
typedef struct AVFrame {
//指向包含音频或视频数据的缓冲区。对于视频帧,每个指针可能指向图像的一个平面,对于音频帧,每个指针可能指向一个通道的数据。
//这些指针应指向在 buf 或 extended_buf 中管理的内存区域。
//当AVFrame 被释放时,指针指向的内存会通过引用计数机制处理,具体取决于 buf 和 extended_buf 的内容。
uint8_t *data[AV_NUM_DATA_POINTERS];
//数组用于描述数据平面的每一行的大小。这可能包括额外的填充字节,用于对齐
int linesize[AV_NUM_DATA_POINTERS];
//extended_data 提供对 data 数组无法容纳的额外数据的访问。
//需要注意的是,extended_data 中的指针也应指向在 buf 或 extended_buf 中管理的内存区域。
uint8_t **extended_data;
//管理data
AVBufferRef *buf[AV_NUM_DATA_POINTERS];
//管理extended_data
AVBufferRef **extended_buf;
//和AVPacket中的opaque_ref类似,此处不赘述
AVBufferRef *opaque_ref;
}
4.4 内存分配
avpacket和avframe差不多,此处不赘述。主要步骤有两步,一是申请一个packet或者frame,另外一个是申请packet和frame中的具体数据。packet的数据大小需要手动指定,frame中的数据大小可以使用**av_frame_get_buffer
**计算出来。
//申请一个packet
AVPacket *av_packet_alloc(void)
{
AVPacket *pkt = av_malloc(sizeof(AVPacket));
if (!pkt)
return pkt;
//默认初始化
get_packet_defaults(pkt);
return pkt;
}
//给packet分配实际数据空间
int av_new_packet(AVPacket *pkt, int size)
{
AVBufferRef *buf = NULL;
int ret = packet_alloc(&buf, size);
if (ret < 0)
return ret;
get_packet_defaults(pkt);
pkt->buf = buf;
pkt->data = buf->data;
pkt->size = size;
return 0;
}
int av_frame_get_buffer(AVFrame *frame, int align)
{
if (frame->format < 0)
return AVERROR(EINVAL);
if (frame->width > 0 && frame->height > 0)
//计算视频buffer
return get_video_buffer(frame, align);
else if (frame->nb_samples > 0 &&
(av_channel_layout_check(&frame->ch_layout)))
//计算音频buffer
return get_audio_buffer(frame, align);
return AVERROR(EINVAL);
}
4.5 增加引用
int av_packet_ref(AVPacket *dst, const AVPacket *src) {
int ret;
dst->buf = NULL;
//拷贝packet中的属性信息
ret = av_packet_copy_props(dst, src);
if (ret < 0)
goto fail;
//如果src中的buf不存在,说明src中不是用引用计数来管理内存的,那只要把数据拷贝过去就可以了
if (!src->buf) {
ret = packet_alloc(&dst->buf, src->size);
if (ret < 0)
goto fail;
av_assert1(!src->size || src->data);
if (src->size)
memcpy(dst->buf->data, src->data, src->size);
dst->data = dst->buf->data;
} else {
//增加对src中buf的引用
dst->buf = av_buffer_ref(src->buf);
if (!dst->buf) {
ret = AVERROR(ENOMEM);
goto fail;
}
dst->data = src->data;
}
dst->size = src->size;
return 0;
fail:
av_packet_unref(dst);
return ret;
}
4.6 减少引用
void av_packet_unref(AVPacket *pkt)
{
//...
av_buffer_unref(&pkt->opaque_ref);
av_buffer_unref(&pkt->buf);
//将pkt的参数信息回复到默认,避免使用已被解引用的packet
get_packet_defaults(pkt);
}
4.7释放内存
void av_packet_free(AVPacket **pkt)
{
if (!pkt || !*pkt)
return;
//释放前先做一次解引用
av_packet_unref(*pkt);
av_freep(pkt);
}