FFmpeg 内存管理一帧背后的引用计数一帧图像不只是一块像素内存在 FFmpeg 里它背后可能挂着解码器、引用计数、buffer pool 和一串“谁还没松手”的生命周期。很多人第一次写 FFmpeg 代码都会被一个问题绊住“我拿到了一个AVFrame如果想保存一份直接memcpy可以吗”答案是可以但经常不是最好的选择。因为 FFmpeg 的很多对象不是“普通结构体 一块裸内存”。它更像一套引用计数系统你看到的是AVFrame真正的大块数据可能在AVBuffer里你以为拷贝了一帧实际可能只是复制了几个指针你以为释放了解码器某个忘记释放的 frame 还可能把整个 buffer pool 顶住。这篇文章就从“拷贝一帧”这个小问题出发聊清楚 FFmpeg 的内存管理模型。1. 想拷贝 AVFrame先问你要拷贝什么AVFrame这个名字很容易误导人。它看起来像“一帧数据”但更准确地说它是一帧媒体数据的描述对象。一个视频AVFrame里通常有这些东西成员 / 概念作用容易误解的点data[]指向各个平面的数据地址例如 Y、U、V它通常只是指针不代表内存归AVFrame结构体本身所有linesize[]每个平面的行跨度不一定等于图像宽度可能有对齐 paddingbuf[]保存数据 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_unrefFFmpeg 的“借书登记表”理解 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 1av_buffer_unref()释放一个引用refcount -1归零时调用 free callbackav_buffer_alloc()分配一块带引用计数的内存常用于 frame/packet 底层数据AVBufferPoolbuffer 池用于复用频繁申请释放的大块内存普通AVBuffer的释放逻辑很好理解分配一块 data用AVBuffer包起来每多一个使用者就多一个AVBufferRef每个使用者结束时av_buffer_unref()refcount 归零时调用释放回调真正释放 data。但AVBufferPool又多了一层“复用”。当解码器需要一帧 buffer 时它可能不是每次都直接向系统 malloc而是阶段行为结果取 bufferav_buffer_pool_get(pool)从池里拿一个可用 buffer没有空闲才新分配frame 使用中AVFrame::buf[]持有AVBufferRefbuffer 不能被回收frame 释放av_frame_unref/free()引用释放buffer 可能回到 poolpool 销毁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 代码时的速查。对象常见创建增加引用 / 复制引用释放引用销毁对象典型坑AVFrameav_frame_alloc()av_frame_ref()/av_frame_clone()av_frame_unref()av_frame_free()只 free 了壳子思维忘了buf[]才是大头AVPacketav_packet_alloc()av_packet_ref()/av_packet_clone()av_packet_unref()av_packet_free()循环读包时忘记av_packet_unref()AVBufferRefav_buffer_alloc()/av_buffer_ref()av_buffer_ref()av_buffer_unref()通常没有单独 free靠 unref多一个 ref 就多一个释放责任AVCodecContextavcodec_alloc_context3()通常不 ref不适用avcodec_free_context()释放 context 不代表外部 frame 引用也会消失AVFormatContextavformat_open_input()通常不 ref不适用avformat_close_input()demux 和 decode 是两套生命周期SwsContextsws_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 frameH.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 前外部是否还持有输出 framecontext 释放不了外部 frame 的引用是否把 shallow ref 当成 deep copy会导致意外共享或释放时机错误如果只能记住一句话我建议记这句在 FFmpeg 里真正的大内存经常不在你手里的结构体里而在它引用的 buffer 里忘记释放一个引用可能拖住一整片池化内存。结尾FFmpeg 的内存管理并不神秘它只是把“谁拥有数据、谁还在使用数据、什么时候能释放数据”这件事做得非常细。AVFrame、AVPacket、AVBufferRef、AVBufferPool看起来是一堆 API背后其实是一套统一的思路核心问题FFmpeg 的回答大块数据要不要反复拷贝尽量不要优先共享引用多个对象共享数据怎么管理引用计数频繁申请释放大块内存怎么办buffer pool 复用什么时候真正释放最后一个引用释放时最容易出问题在哪里异常路径、覆盖旧指针、成功/失败语义混乱写 FFmpeg最怕的不是少写一个free()而是少想清楚一次“这个引用现在归谁”。