FFmpeg视频播放的内存管理

栏目: IOS · 发布时间: 5年前

内容简介:在写使用ffmpeg版本是3.4对AVFrame:

在写 这个播放器 的时候,遇到了一些内存管理的问题,虽然棘手但是也让我对此有了比较完善的理解,而且很多相关资料并没有跟随FFmpeg的更新,比如缓冲池 AVBufferPool 的使用。

使用ffmpeg版本是3.4

AVFrame和AVPacket的内存管理策略

对AVFrame:

  • av_frame_alloc 只是给 AVFrame 分配了内存,它内部的buf还是空的,就相当于造了一个箱子,但箱子里是空的。
  • av_frame_ref 对src的buf增加一个引用,即使用同一个数据,只是这个数据引用计数+1. av_frame_unref 把自身对buf的引用释放掉,数据的引用计数-1。
  • av_frame_free 内部还是调用了unref,只是把传入的frame也置空。

发现还缺了一个buffer初始化的方法,初始化就在解码函数 avcodec_send_packetavcodec_receive_frame 内部。

然后对于解码有个坑,对 avcodec_receive_frame 函数:

Note that the function will always call av_frame_unref(frame) before doing anything else.

如果你使用同一个frame,每次去接收解码后的数据,那么每次传进去就会把前面的数据释放掉,导致就只有一个frame是有用的。

如果你觉得frame的alloc花费很大,想节省资源,然后又没注意到这个注释的话,很可能就会这么做。

对此有两种方案:

av_frame_ref
avcodec_receive_frame

方便来说,是第二种方案好;但从模块化角度说,是第一种的更好,单解码这一步,要自己管理好自己的内存,即buffer的alloc和unref配套。这样内存的管理在当前的模块内部是完善的,如果出了问题,也只是其他模块出了问题。相比而言,第一种就是把内存的释放依赖在了其他模块的处理上。

AVPacket基本和AVFrame一致,只是获取packet的函数 av_read_frame 它并不会执行unref操作,而是直接把buf设为null。使用上面的两个方案之一也都可以规避这个问题。

不管怎样,直接的frame1=frame2这样的赋值是不可取的。当然要具体问题具体分析,时刻注意它内部是用引用计数的方式管理buf内的数据。

一点都没释放

最开始是播放停止后的内存几乎没有下降,解码后的 AVFrame 是用一个缓冲区来管理的,里面的frame是暂存没释放的,我以为是这个缓冲区里有留存,然后给它添加了释放方法,结束后每个frame都调用 av_packet_free ,然后奇怪的事出现了。

很明确每个frame都调用了free或者unref,但是内存却没什么改变。哪怕释放不干净,至少要少一点吧。难道是 av_packet_free 不起作用?我试着把播放完的frame的free取消,但内存在播放的时候就飙涨了,说明这个是有用的。

然后缓冲区有个最大数量限制,调大这个数量,内存就上涨,调小就下降。这可以理解,因为这里面的frame都是存在的,所以肯定会占内存。

结合上面一起就是:在结束播放后,缓冲区里的frame集体没有释放,一个都没有!

怎么查?看源码。

av_frame_free 看,这个里面起作用的还是 av_frame_unref ,它的源码:

void av_buffer_unref(AVBufferRef **buf)
	{
	    if (!buf || !*buf)
	        return;
	
	    buffer_replace(buf, NULL);
	}
	
	 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);
	    }
	}
复制代码

所以关键点就是 atomic_fetch_add_explicit ,这个函数有一个系列,就是进行 原子性 的加减乘除的,这个函数是先 fetchadd ,先查询再增加,所以返回的值是修改之前的。

atomic_fetch_add_explicit(&b->refcount, -1, memory_order_acq_rel) == 1 整句代码就是:如果当前引用计数为1,就释放数据,因为加-1,所以条件等价于引用计数为0。

AVFrame和AVPacket的重量级数据都存在它们的buf里,data和extend_data都是从数据里引用过来的,buf是 AVBufferRef 类型,表示一个对于 AVBuffer 的引用,多一个引用, AVBuffer 的引用计数就+1,少一个就-1,没有引用就释放, AVBuffer 是数据的真身。对于AVFrame和AVPacket的内存管理就是依赖 av_xxx_refav_xxx_unref 这一套函数。

然后就是看一下 b->free(b->opaque, b->data); 这个具体调用了什么函数。在 AVBuffer 的文档里有个 void av_buffer_default_free(void *opaque, uint8_t *data); ,说是默认的释放函数,在释放 AVBuffer 时调用这个函数。这个函数就是调用了 av_free ,而 av_free 就是调用了 free ,也就是单纯的释放内存罢了。

如果 b->free(b->opaque, b->data); 真的是调用了这个默认的释放函数,那么内存一定会下降的。 这里有个帮助很大但不知道原理的东西,就是Synbolic断点可以自动定位到源码,而且可以查看调用栈数据,相关知识只能查到这个 。这样就可以在运行的时候直接看到 b->free 是什么东西了,它是 pool_release_buffer !!!

static void pool_release_buffer(void *opaque, uint8_t *data)
{
   BufferPoolEntry *buf = opaque;
   AVBufferPool *pool = buf->pool;
   ...
   if (atomic_fetch_add_explicit(&pool->refcount, -1, memory_order_acq_rel) == 1)
       buffer_pool_free(pool);

复制代码

这里面根本没有释放data的地方,同样是引用计数操作,然后到 buffer_pool_free

/*
* This function gets called when the pool has been uninited and
* all the buffers returned to it.
*/
static void buffer_pool_free(AVBufferPool *pool)
{
   while (pool->pool) {
       BufferPoolEntry *buf = pool->pool;
       pool->pool = buf->next;

       buf->free(buf->opaque, buf->data);
       av_freep(&buf);
   }
   ff_mutex_destroy(&pool->mutex);

   if (pool->pool_free)
       pool->pool_free(pool->opaque);

   av_freep(&pool);
}
复制代码

结合这个函数、pool这个名字还有上面那两行注释,以及我的测试可以得出:

  • pool是一个缓冲池,管理者众多的内存缓冲区(AVBuffer)
  • 从池里生成的buffer,在释放的时候,是再回到池里,并且池的引用计数-1。也就是这是一个循环使用的缓冲池,使用引用计数来 标记 内部的缓冲区。
  • 池构建( av_buffer_pool_init )的时候,引用计数为初始值1,调用 av_buffer_pool_uninit 标记为可销毁,引用计数减1,这两者刚好匹配。
  • 内部每生成一个buffer,引用计数+1,回收一个buffer,引用计数-1。这两者也是匹配的。
  • 结合上两点,只要合理操作,内存就可以得到释放。而没有释放,至少有一个没做到。
  • 循环缓冲池的作用是为了避免频繁的、大量的内存分配和释放,特别是视频帧数据,一帧就上百k。同时也解释了为什么内存一点都没有释放,使用了池,要么全部释放,要么一点都不释放。

从内部再回到外部,先检查是否有frame没有释放。这时确实是有的,就在:

retval = avcodec_receive_frame(decoder->codecCtx, frame);
            
if (retval != 0) {
    TFCheckRetval("avcodec receive frame");
    av_frame_free(&frame);//漏掉了这里
    continue;
 }
复制代码

在解码失败后,就直接 continue 了。在意识里,好像这里的frame是无用的,没数据的,所以就直接忽略了,接下一个。就死在了这里。

在把这种的frame都释放时候,还是有问题,就剩下 av_buffer_pool_uninit 这个了。这个函数的调用里用户使用的外层很远,最终查到是从 avcodec_close 这里进入的。在逻辑也是合理的,解码结束了,才需要把分配的内存销毁。但是不要直接调用 avcodec_close ,而是使用 avcodec_free_context ,把codec相关的其他东西一并释放了。

到这,终于内存释放了。重点在于认识到有个pool的存在,这个在网上资料并不多。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Agile Web Development with Rails 4

Agile Web Development with Rails 4

Sam Ruby、Dave Thomas、David Heinemeier Hansson / Pragmatic Bookshelf / 2013-10-11 / USD 43.95

Ruby on Rails helps you produce high-quality, beautiful-looking web applications quickly. You concentrate on creating the application, and Rails takes care of the details. Tens of thousands of deve......一起来看看 《Agile Web Development with Rails 4》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

URL 编码/解码
URL 编码/解码

URL 编码/解码

html转js在线工具
html转js在线工具

html转js在线工具