FFmpeg視訊播放的記憶體管理
在寫ofollow,noindex">這個播放器
的時候,遇到了一些記憶體管理的問題,雖然棘手但是也讓我對此有了比較完善的理解,而且很多相關資料並沒有跟隨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_packet
和avcodec_receive_frame
內部。
然後對於解碼有個坑,對avcodec_receive_frame
函式:
Note that the function will always callav_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
,這個函式有一個系列,就是進行原子性
的加減乘除的,這個函式是先fetch
再add
,先查詢再增加,所以返回的值是修改之前的。
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_ref
和av_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的存在,這個在網上資料並不多。