FFmpeg内存管理:一帧背后的引用计数
FFmpeg 内存管理:一帧背后的引用计数
一帧图像不只是一块像素内存:在 FFmpeg 里,它背后可能挂着解码器、引用计数、buffer pool 和一串“谁还没松手”的生命周期。
很多人第一次写 FFmpeg 代码,都会被一个问题绊住:
“我拿到了一个 AVFrame,如果想保存一份,直接 memcpy 可以吗?”
答案是:可以,但经常不是最好的选择。
因为 FFmpeg 的很多对象不是“普通结构体 + 一块裸内存”。它更像一套引用计数系统:你看到的是 AVFrame,真正的大块数据可能在 AVBuffer 里;你以为拷贝了一帧,实际可能只是复制了几个指针;你以为释放了解码器,某个忘记释放的 frame 还可能把整个 buffer pool 顶住。
这篇文章就从“拷贝一帧”这个小问题出发,聊清楚 FFmpeg 的内存管理模型。

1. 想拷贝 AVFrame,先问:你要拷贝什么?
AVFrame 这个名字很容易误导人。它看起来像“一帧数据”,但更准确地说,它是一帧媒体数据的描述对象。
一个视频 AVFrame 里通常有这些东西:
| 成员 / 概念 | 作用 | 容易误解的点 |
|---|---|---|
data[] |
指向各个平面的数据地址,例如 Y、U、V | 它通常只是指针,不代表内存归 AVFrame 结构体本身所有 |
linesize[] |
每个平面的行跨度 | 不一定等于图像宽度,可能有对齐 padding |
buf[] |
保存数据 buffer 的引用 | 这是释放大块像素内存的关键引用 |
extended_buf |
超过固定数组数量时的额外 buffer 引用 | 音频多声道等场景常见 |
width/height/format |
描述图像尺寸和像素格式 | 只描述数据,不负责管理底层内存 |
pts/pkt_dts |
时间戳信息 | 拷贝像素时不一定要保留,做同步时必须关注 |
所以“拷贝 AVFrame”至少有两种含义:
| 需求 | 典型做法 | 结果 |
|---|---|---|
| 只想多持有一份引用 | av_frame_ref(dst, src) |
快,通常不复制像素;底层 buffer 引用计数 +1 |
| 想要独立像素副本 | av_frame_clone() 后必要时 av_frame_make_writable(),或新分配 frame 后拷贝数据 |
更安全,但可能产生真实内存拷贝 |
| 只想临时传递所有权 | av_frame_move_ref(dst, src) |
不复制数据,把引用从 src 转移到 dst |
| 只想拷贝元信息 | av_frame_copy_props(dst, src) |
只复制时间戳、色彩信息等属性,不复制像素 |
这里最重要的区别是:
av_frame_ref()不是深拷贝像素,它是在共享底层 buffer 的前提下增加引用。
这正是 FFmpeg 高效的地方:高清视频一帧动辄几 MB,如果每个模块都深拷贝一次,内存和 CPU 很快就会爆炸。FFmpeg 默认更倾向于“共享数据 + 引用计数”。
2. xxx_ref / xxx_unref:FFmpeg 的“借书登记表”
理解 FFmpeg 内存管理,先抓住一组命名习惯:
| API 形态 | 含义 | 你可以怎么理解 |
|---|---|---|
xxx_alloc() |
创建对象壳子或分配对象 | 从仓库拿一个新对象 |
xxx_free() |
销毁对象,并通常把指针置空 | 彻底归还对象 |
xxx_ref() |
增加一份引用 | 多登记一个借阅人 |
xxx_unref() |
释放当前引用 | 当前借阅人还书 |
xxx_move_ref() |
转移引用所有权 | 把借书卡从 A 换到 B,不新增借阅人 |
xxx_clone() |
创建新对象并引用同一份底层数据 | 新建一个壳子,但底层书可能还是同一本 |
拿 AVFrame 来说:
| 操作 | 会发生什么 | 注意点 |
|---|---|---|
av_frame_alloc() |
只分配 AVFrame 结构体 |
不一定分配像素数据 |
av_frame_get_buffer() |
给 frame 分配底层数据 buffer | 数据通常挂在 buf[] 引用里 |
av_frame_ref(dst, src) |
dst 引用 src 的底层 buffer |
两个 frame 共享数据;引用计数增加 |
av_frame_unref(frame) |
释放 frame 当前持有的 buffer 引用 | frame 壳子还在,可以复用 |
av_frame_free(&frame) |
先 unref,再释放 frame 结构体,并置空指针 | 最常见的收尾方式 |
这里的设计非常像“借书登记表”:
| 角色 | 类比 | 真实对象 |
|---|---|---|
| 书 | 真正占空间的数据 | 像素 buffer / packet data |
| 借书卡 | 一个引用 | AVBufferRef |
| 借阅登记表 | 引用计数 | AVBuffer 内部 refcount |
| 读者 | 持有引用的对象 | AVFrame、AVPacket、解码器内部结构 |
| 图书馆 | 管理 buffer 的地方 | AVBufferPool 或普通 allocator |
只有最后一个读者还书,书才真正能从系统里移走。
3. AVBuffer:真正管理大块内存的底座
在 FFmpeg 里,很多大块数据最终都会落到 AVBuffer / AVBufferRef 这套机制上。
简单说:
| 名称 | 作用 | 重点 |
|---|---|---|
AVBuffer |
真正管理底层 data 和引用计数的对象 | 里面知道 data 怎么释放 |
AVBufferRef |
指向 AVBuffer 的引用句柄 |
可以有多个 ref 指向同一块数据 |
av_buffer_ref() |
复制一个引用 | refcount +1 |
av_buffer_unref() |
释放一个引用 | refcount -1,归零时调用 free callback |
av_buffer_alloc() |
分配一块带引用计数的内存 | 常用于 frame/packet 底层数据 |
AVBufferPool |
buffer 池 | 用于复用频繁申请释放的大块内存 |
普通 AVBuffer 的释放逻辑很好理解:
- 分配一块 data;
- 用
AVBuffer包起来; - 每多一个使用者,就多一个
AVBufferRef; - 每个使用者结束时
av_buffer_unref(); - refcount 归零时调用释放回调,真正释放 data。
但 AVBufferPool 又多了一层“复用”。

当解码器需要一帧 buffer 时,它可能不是每次都直接向系统 malloc,而是:
| 阶段 | 行为 | 结果 |
|---|---|---|
| 取 buffer | av_buffer_pool_get(pool) |
从池里拿一个可用 buffer;没有空闲才新分配 |
| frame 使用中 | AVFrame::buf[] 持有 AVBufferRef |
buffer 不能被回收 |
| frame 释放 | av_frame_unref/free() |
引用释放,buffer 可能回到 pool |
| pool 销毁 | pool 自身引用也释放,且没有外部 buffer ref | 池里缓存的底层内存才真正 free |
这就是很多 FFmpeg 内存问题难查的地方:
av_frame_free()不一定等于“立刻把像素内存还给系统”,它可能只是把 buffer 还给池。真正释放要看 pool 是否销毁,以及所有引用是否都归零。
4. 为什么 FFmpeg 要这么设计?
因为视频处理太吃内存,也太吃拷贝成本。
一帧 1080p YUV420P 8bit 大概 3MB;如果是 10bit,接近 6MB。4K、10bit、多线程解码时,这个数字还会继续上升。
如果每个环节都做深拷贝,典型链路会一路膨胀:
| 环节 | 如果深拷贝 | 直接后果 |
|---|---|---|
| 解码输出 | 产出第一份像素数据 | 基础帧内存已经产生 |
| 滤镜处理 | 再拷贝一份给滤镜 | CPU 多一次大块内存读写 |
| 缩放处理 | 再拷贝一份给缩放器 | 临时峰值继续抬高 |
| 渲染显示 | 再拷贝一份给渲染层 | 帧率和功耗都受影响 |
| 缓存复用 | 再拷贝一份给缓存 | 多路播放、批量缩略图时更容易爆内存 |
这条链路真正的问题可以概括成两类:
| 问题 | 后果 | 典型表现 |
|---|---|---|
| CPU 拷贝成本高 | 帧率下降、耗电上升 | 高清视频、实时滤镜更明显 |
| 瞬时内存膨胀 | 多份大 frame 同时存在 | 多路播放、缩略图批处理时尤其明显 |
所以 FFmpeg 更偏向“共享引用,而不是层层复制”的模型:
| 设计选择 | 做法 | 收益 |
|---|---|---|
| 数据只放一份 | 像素数据放在底层 buffer 里 | 避免重复存储大块内存 |
| 对象通过 ref 共享 | AVFrame / AVPacket 持有引用 |
模块间传递更轻量 |
| 用完主动 unref | 谁增加引用,谁负责释放引用 | 生命周期可追踪 |
| 最后一个引用释放 | refcount 归零后释放底层内存 | 保证没人使用时再回收 |
这套模型很强,但也有代价:你必须认真对待每一个 ref 和 unref。
5. 常见对象的 ref / unref 心智模型
下面这张表可以作为写 FFmpeg 代码时的速查。
| 对象 | 常见创建 | 增加引用 / 复制引用 | 释放引用 | 销毁对象 | 典型坑 |
|---|---|---|---|---|---|
AVFrame |
av_frame_alloc() |
av_frame_ref() / av_frame_clone() |
av_frame_unref() |
av_frame_free() |
只 free 了壳子思维,忘了 buf[] 才是大头 |
AVPacket |
av_packet_alloc() |
av_packet_ref() / av_packet_clone() |
av_packet_unref() |
av_packet_free() |
循环读包时忘记 av_packet_unref() |
AVBufferRef |
av_buffer_alloc() / av_buffer_ref() |
av_buffer_ref() |
av_buffer_unref() |
通常没有单独 free,靠 unref | 多一个 ref 就多一个释放责任 |
AVCodecContext |
avcodec_alloc_context3() |
通常不 ref | 不适用 | avcodec_free_context() |
释放 context 不代表外部 frame 引用也会消失 |
AVFormatContext |
avformat_open_input() |
通常不 ref | 不适用 | avformat_close_input() |
demux 和 decode 是两套生命周期 |
SwsContext |
sws_getContext() |
不适用 | 不适用 | sws_freeContext() |
尺寸/格式变化时旧 context 忘释放 |
一个很实用的习惯是:谁拿到 ownership,谁负责释放;谁调用了 ref,谁就必须配一个 unref。
6. 忘记释放一个 AVFrame,为什么可能不只漏“一帧”?
现在回到最开头的问题:如果一个 AVFrame 忘记释放,会发生什么?
直觉上,很多人会以为只是漏了一个结构体:
sizeof(AVFrame) 几百字节?小问题。
真实情况完全不是这样。
AVFrame 自己只是壳。它可能通过 buf[] 挂着几 MB 的像素内存;如果这些 buffer 来自 AVBufferPool,漏掉一个 frame 引用,还可能让整个 pool 没法销毁。

后果可以分三层:
| 层级 | 泄漏内容 | 影响 |
|---|---|---|
| 第一层 | AVFrame 结构体本身 |
很小,通常不是主因 |
| 第二层 | AVFrame::buf[] 持有的像素 buffer |
可能是几 MB 到几十 MB |
| 第三层 | AVBufferPool 因引用未归零无法销毁 |
可能拖住一批已归还 pool 的大块 buffer |
这也是为什么某些问题看起来只是“漏了一帧”,最后却表现成几百 MB 甚至 GB 级 native 内存增长。
尤其在这些场景里,放大效应更明显:
| 场景 | 为什么更危险 |
|---|---|
| 批量缩略图 | 每个视频都要解一帧,次数多,容易线性累积 |
| 10bit / 4K 视频 | 单帧内存更大 |
| 多线程解码 | 每个线程可能持有 delayed frame / reference frame |
| H.264 / HEVC | 解码器内部有参考帧队列和 DPB |
| 异常 fallback | 正常路径释放了,错误路径反而容易漏 |
所以 FFmpeg 内存排查里,一个非常重要的原则是:
不要只看“我最后有没有 free codec context”,还要看中间所有 frame / packet 的引用有没有归零。
avcodec_free_context() 能释放解码器自己持有的资源,但它不能替你释放已经丢到外面的 AVFrame。如果某个 AVFrame 指针被覆盖、丢失、或者异常路径漏掉,里面的 AVBufferRef 仍然可能让底层 buffer 活着。
7. 写 FFmpeg 代码的几个自检问题
最后给一组很实用的检查清单。每次写到 AVFrame / AVPacket 生命周期时,可以顺手过一遍。
| 自检问题 | 为什么要问 |
|---|---|
每个 av_frame_alloc() 是否都有对应 av_frame_free()? |
frame 壳子和其中的 buffer 引用都要释放 |
每次循环复用 AVFrame* 前,旧值是否已经 unref/free? |
防止旧指针被覆盖后永久失联 |
每个 av_frame_ref() 是否都有对应 av_frame_unref()? |
引用计数不归零,底层 buffer 就不能释放 |
错误路径、continue、break 前是否清理了 frame? |
内存泄漏最常出现在异常分支 |
| 成功码和失败码是否可能冲突? | 成功被误判失败时,资源释放路径可能被绕过 |
AVPacket 循环读包后是否及时 av_packet_unref()? |
packet 也常持有引用计数 buffer |
| 释放 codec context 前,外部是否还持有输出 frame? | context 释放不了外部 frame 的引用 |
| 是否把 shallow ref 当成 deep copy? | 会导致意外共享或释放时机错误 |
如果只能记住一句话,我建议记这句:
在 FFmpeg 里,真正的大内存经常不在你手里的结构体里,而在它引用的 buffer 里;忘记释放一个引用,可能拖住一整片池化内存。
结尾
FFmpeg 的内存管理并不神秘,它只是把“谁拥有数据、谁还在使用数据、什么时候能释放数据”这件事做得非常细。
AVFrame、AVPacket、AVBufferRef、AVBufferPool 看起来是一堆 API,背后其实是一套统一的思路:
| 核心问题 | FFmpeg 的回答 |
|---|---|
| 大块数据要不要反复拷贝? | 尽量不要,优先共享引用 |
| 多个对象共享数据怎么管理? | 引用计数 |
| 频繁申请释放大块内存怎么办? | buffer pool 复用 |
| 什么时候真正释放? | 最后一个引用释放时 |
| 最容易出问题在哪里? | 异常路径、覆盖旧指针、成功/失败语义混乱 |
写 FFmpeg,最怕的不是少写一个 free(),而是少想清楚一次“这个引用现在归谁”。
更多推荐


所有评论(0)