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[]每个平面的行跨度不一定等于图像宽度可能有对齐 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()而是少想清楚一次“这个引用现在归谁”。

相关新闻

机器人手术技术现状与挑战:从达芬奇系统到AI辅助的工程实践

机器人手术技术现状与挑战:从达芬奇系统到AI辅助的工程实践

1. 这篇文章真正要解决的问题当马斯克在社交媒体上谈论机器人手术时,他描绘的图景往往是“未来几年内,外科医生将被淘汰”、“机器人将自主完成所有复杂手术”。这类言论在科技圈总能引发巨大关注,但也让许多医疗技术从业者和一线开发者感到困…

2026/9/15 18:12:12 阅读更多 →
FutureBridge-OPD:基于前瞻性验证的主动知识蒸馏框架

FutureBridge-OPD:基于前瞻性验证的主动知识蒸馏框架

大家好,我是专注于AI模型优化与部署的技术博主。在模型压缩与加速的实践中,知识蒸馏是一种极为有效的手段,但传统的蒸馏方法往往让学生模型被动地模仿教师模型,缺乏对教师建议有效性的主动判断。今天,我们将深入探讨一…

2026/9/10 4:07:32 阅读更多 →
Bilibili-Evolved:免费开源工具彻底改造你的哔哩哔哩体验

Bilibili-Evolved:免费开源工具彻底改造你的哔哩哔哩体验

Bilibili-Evolved:免费开源工具彻底改造你的哔哩哔哩体验 【免费下载链接】Bilibili-Evolved 强大的哔哩哔哩增强脚本 项目地址: https://gitcode.com/gh_mirrors/bi/Bilibili-Evolved 你是否对B站的标准界面感到审美疲劳?是否希望拥有更强大的视…

2026/9/6 7:23:31 阅读更多 →

最新新闻

Kafka核心原理与实战:分区、副本、消息可靠性及面试要点

Kafka核心原理与实战:分区、副本、消息可靠性及面试要点

先说点实际的。Kafka 这东西,几乎所有做后端或者数据平台的人早晚都会碰到。不管你是搞日志采集、用户行为追踪、消息削峰,还是做实时数仓,Kafka 几乎成了事实标准。我最早接触它的时候,还是因为业务量上来之后,Active…

2026/9/16 3:11:20 阅读更多 →
Node-RED Status节点:上位机实时状态监测与告警实战指南

Node-RED Status节点:上位机实时状态监测与告警实战指南

直接开工。这篇主要聊我在实际项目里怎么用 Node-RED 搭上位机逻辑,以及 Status 节点在整个链路里到底起到什么作用。如果你只是把 Node-RED 当成一个协议转换工具,那 Status 节点确实没什么存在感;但一旦你要做真正的上位机——需要实时感知…

2026/9/16 3:11:20 阅读更多 →
基于STM32的智能风扇设计:DHT11测温与PWM调速实战

基于STM32的智能风扇设计:DHT11测温与PWM调速实战

简介:一套基于STM32F103的智能风扇设计源码工程,面向嵌入式入门者与电子设计爱好者,覆盖人体感应、温湿度检测与PWM无级调速等典型功能场景。压缩包共315个文件,大小约16.96MB,主要包含C源文件、头文件、编译过程产生的…

2026/9/16 3:11:20 阅读更多 →
Java Swing游戏开发实战:从马里奥物理引擎到状态机设计

Java Swing游戏开发实战:从马里奥物理引擎到状态机设计

简介:这是一份基于Java实现的经典超级马里奥风格小游戏源码,面向计算机、数学、电子信息等专业的本科生,适用于课程设计、期末大作业及毕业设计参考。项目采用纯Java Swing开发,不依赖第三方游戏引擎,有助于深入理解图…

2026/9/16 3:11:20 阅读更多 →
酷开A2系列5S07机芯V017.009.060稳定版整机USB刷机教程

酷开A2系列5S07机芯V017.009.060稳定版整机USB刷机教程

简介:酷开智能电视A2系列(55A2、50A2)5S07机芯的整机USB升级固件,稳定版V017.009.060。这份刷机升级包面向电视维修人员、数码爱好者以及需要自行解决系统问题的家庭用户,可解决系统卡顿、应用闪退、开机异常等常见故障…

2026/9/16 3:11:20 阅读更多 →
Java单链表核心操作:反转、环检测与哨兵技巧图解

Java单链表核心操作:反转、环检测与哨兵技巧图解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/16 3:10:20 阅读更多 →

日新闻

嵌入式三大高薪赛道:车规功能安全、RISC-V固件架构、边缘AI部署

嵌入式三大高薪赛道:车规功能安全、RISC-V固件架构、边缘AI部署

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/16 0:00:51 阅读更多 →
IoT-For-Beginners 智能语音计时器:Wio Terminal 基于 DMAC 与 Flash 的音频采集实战

IoT-For-Beginners 智能语音计时器:Wio Terminal 基于 DMAC 与 Flash 的音频采集实战

IoT-For-Beginners 智能语音计时器:Wio Terminal 基于 DMAC 与 Flash 的音频采集实战 【免费下载链接】IoT-For-Beginners 12 Weeks, 24 Lessons, IoT for All! 项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners 本指南聚焦 GitHub Tren…

2026/9/16 0:01:52 阅读更多 →
基于MATLAB的CRI显色指数计算:从SPD光谱到Ra的完整流程

基于MATLAB的CRI显色指数计算:从SPD光谱到Ra的完整流程

简介:针对照明设计与光学研究中的光谱功率分布(SPD)与显色性指数(CRI)计算需求,这套MATLAB程序为照明工程师、LED研发人员及光学专业学生提供了轻量工具。代码通过解析光谱测量数据,自动完成波长…

2026/9/16 0:01:52 阅读更多 →

周新闻

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验 【免费下载链接】ai The AI Toolkit for TypeScript. From the creators of Next.js, the AI SDK is a free open-source library for building AI-powered applications and ag…

2026/9/15 12:27:42 阅读更多 →
Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化

Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化

Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化 【免费下载链接】refine A React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility. 项目地址: https://gitcode.com/GitH…

2026/9/16 1:59:46 阅读更多 →
Flutter应用改名全指南:从Android到iOS的配置与工具实践

Flutter应用改名全指南:从Android到iOS的配置与工具实践

刚接一个外包项目时,甲方要求把工程里临时用的应用名改成正式产品名。我本来觉得“改名”这种小事,打开配置文件改一行不就完了?结果真动手才发现,Flutter项目里“应用名称”根本不是一处配置,而是一整套散落在 Androi…

2026/9/16 1:59:35 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/15 21:40:00 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/15 21:39:18 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/15 21:40:17 阅读更多 →