FFmpeg視頻播放的內存管理

在寫這個播放器的時候,遇到了一些內存管理的問題,雖然棘手可是也讓我對此有了比較完善的理解,並且不少相關資料並無跟隨FFmpeg的更新,好比緩衝池AVBufferPool的使用。ios

使用ffmpeg版本是3.4git

AVFrame和AVPacket的內存管理策略

對AVFrame:github

  • 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內部。xcode

而後對於解碼有個坑,對avcodec_receive_frame函數:bash

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

若是你使用同一個frame,每次去接收解碼後的數據,那麼每次傳進去就會把前面的數據釋放掉,致使就只有一個frame是有用的。函數

若是你以爲frame的alloc花費很大,想節省資源,而後又沒注意到這個註釋的話,極可能就會這麼作。visual-studio

對此有兩種方案:測試

  1. 繼續只使用一個frame來接收,可是在傳遞給下一步(渲染、播放等)的時候,下一步的模塊使用一個新的frame和av_frame_ref來接收,而不是直接的賦值。
  2. 每次解碼前構建一個新的AVFrame,把它傳給avcodec_receive_frame,這樣每次都是新的frame,互不干擾。可是在整個流程結束時,要釋放這個frame.

方便來講,是第二種方案好;但從模塊化角度說,是第一種的更好,單解碼這一步,要本身管理好本身的內存,即buffer的alloc和unref配套。這樣內存的管理在當前的模塊內部是完善的,若是出了問題,也只是其餘模塊出了問題。相比而言,第一種就是把內存的釋放依賴在了其餘模塊的處理上。fetch

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的存在,這個在網上資料並很少。

相關文章
相關標籤/搜索