在寫這個播放器的時候,遇到了一些內(nèi)存管理的問題,雖然棘手但是也讓我對此有了比較完善的理解,而且很多相關(guān)資料并沒有跟隨FFmpeg的更新,比如緩沖池AVBufferPool
的使用。
使用ffmpeg版本是3.4
AVFrame和AVPacket的內(nèi)存管理策略
對AVFrame:
-
av_frame_alloc
只是給AVFrame
分配了內(nèi)存,它內(nèi)部的buf還是空的,就相當(dāng)于造了一個箱子,但箱子里是空的。 -
av_frame_ref
對src的buf增加一個引用,即使用同一個數(shù)據(jù),只是這個數(shù)據(jù)引用計(jì)數(shù)+1.av_frame_unref
把自身對buf的引用釋放掉,數(shù)據(jù)的引用計(jì)數(shù)-1。 -
av_frame_free
內(nèi)部還是調(diào)用了unref,只是把傳入的frame也置空。
發(fā)現(xiàn)還缺了一個buffer初始化的方法,初始化就在解碼函數(shù)avcodec_send_packet
和avcodec_receive_frame
內(nèi)部。
然后對于解碼有個坑,對avcodec_receive_frame
函數(shù):
Note that the function will always call
av_frame_unref(frame)
before doing anything else.
如果你使用同一個frame,每次去接收解碼后的數(shù)據(jù),那么每次傳進(jìn)去就會把前面的數(shù)據(jù)釋放掉,導(dǎo)致就只有一個frame是有用的。
如果你覺得frame的alloc花費(fèi)很大,想節(jié)省資源,然后又沒注意到這個注釋的話,很可能就會這么做。
對此有兩種方案:
- 繼續(xù)只使用一個frame來接收,但是在傳遞給下一步(渲染、播放等)的時候,下一步的模塊使用一個新的frame和
av_frame_ref
來接收,而不是直接的賦值。 - 每次解碼前構(gòu)建一個新的AVFrame,把它傳給
avcodec_receive_frame
,這樣每次都是新的frame,互不干擾。但是在整個流程結(jié)束時,要釋放這個frame.
方便來說,是第二種方案好;但從模塊化角度說,是第一種的更好,單解碼這一步,要自己管理好自己的內(nèi)存,即buffer的alloc和unref配套。這樣內(nèi)存的管理在當(dāng)前的模塊內(nèi)部是完善的,如果出了問題,也只是其他模塊出了問題。相比而言,第一種就是把內(nèi)存的釋放依賴在了其他模塊的處理上。
AVPacket基本和AVFrame一致,只是獲取packet的函數(shù)av_read_frame
它并不會執(zhí)行unref操作,而是直接把buf設(shè)為null。使用上面的兩個方案之一也都可以規(guī)避這個問題。
不管怎樣,直接的frame1=frame2這樣的賦值是不可取的。當(dāng)然要具體問題具體分析,時刻注意它內(nèi)部是用引用計(jì)數(shù)的方式管理buf內(nèi)的數(shù)據(jù)。
一點(diǎn)都沒釋放
最開始是播放停止后的內(nèi)存幾乎沒有下降,解碼后的AVFrame
是用一個緩沖區(qū)來管理的,里面的frame是暫存沒釋放的,我以為是這個緩沖區(qū)里有留存,然后給它添加了釋放方法,結(jié)束后每個frame都調(diào)用av_packet_free
,然后奇怪的事出現(xiàn)了。
很明確每個frame都調(diào)用了free或者unref,但是內(nèi)存卻沒什么改變。哪怕釋放不干凈,至少要少一點(diǎn)吧。難道是av_packet_free
不起作用?我試著把播放完的frame的free取消,但內(nèi)存在播放的時候就飆漲了,說明這個是有用的。
然后緩沖區(qū)有個最大數(shù)量限制,調(diào)大這個數(shù)量,內(nèi)存就上漲,調(diào)小就下降。這可以理解,因?yàn)檫@里面的frame都是存在的,所以肯定會占內(nèi)存。
結(jié)合上面一起就是:在結(jié)束播放后,緩沖區(qū)里的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);
}
}
所以關(guān)鍵點(diǎn)就是atomic_fetch_add_explicit
,這個函數(shù)有一個系列,就是進(jìn)行原子性的加減乘除的,這個函數(shù)是先fetch
再add
,先查詢再增加,所以返回的值是修改之前的。
atomic_fetch_add_explicit(&b->refcount, -1, memory_order_acq_rel) == 1
整句代碼就是:如果當(dāng)前引用計(jì)數(shù)為1,就釋放數(shù)據(jù),因?yàn)榧?1,所以條件等價(jià)于引用計(jì)數(shù)為0。
AVFrame和AVPacket的重量級數(shù)據(jù)都存在它們的buf里,data和extend_data都是從數(shù)據(jù)里引用過來的,buf是
AVBufferRef
類型,表示一個對于AVBuffer
的引用,多一個引用,AVBuffer
的引用計(jì)數(shù)就+1,少一個就-1,沒有引用就釋放,AVBuffer
是數(shù)據(jù)的真身。對于AVFrame和AVPacket的內(nèi)存管理就是依賴av_xxx_ref
和av_xxx_unref
這一套函數(shù)。
然后就是看一下b->free(b->opaque, b->data);
這個具體調(diào)用了什么函數(shù)。在AVBuffer
的文檔里有個void av_buffer_default_free(void *opaque, uint8_t *data);
,說是默認(rèn)的釋放函數(shù),在釋放AVBuffer
時調(diào)用這個函數(shù)。這個函數(shù)就是調(diào)用了av_free
,而av_free
就是調(diào)用了free
,也就是單純的釋放內(nèi)存罷了。
如果b->free(b->opaque, b->data);
真的是調(diào)用了這個默認(rèn)的釋放函數(shù),那么內(nèi)存一定會下降的。這里有個幫助很大但不知道原理的東西,就是Synbolic斷點(diǎn)可以自動定位到源碼,而且可以查看調(diào)用棧數(shù)據(jù),相關(guān)知識只能查到這個。這樣就可以在運(yùn)行的時候直接看到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的地方,同樣是引用計(jì)數(shù)操作,然后到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);
}
結(jié)合這個函數(shù)、pool這個名字還有上面那兩行注釋,以及我的測試可以得出:
- pool是一個緩沖池,管理者眾多的內(nèi)存緩沖區(qū)(AVBuffer)
- 從池里生成的buffer,在釋放的時候,是再回到池里,并且池的引用計(jì)數(shù)-1。也就是這是一個循環(huán)使用的緩沖池,使用引用計(jì)數(shù)來標(biāo)記內(nèi)部的緩沖區(qū)。
- 池構(gòu)建(
av_buffer_pool_init
)的時候,引用計(jì)數(shù)為初始值1,調(diào)用av_buffer_pool_uninit
標(biāo)記為可銷毀,引用計(jì)數(shù)減1,這兩者剛好匹配。 - 內(nèi)部每生成一個buffer,引用計(jì)數(shù)+1,回收一個buffer,引用計(jì)數(shù)-1。這兩者也是匹配的。
- 結(jié)合上兩點(diǎn),只要合理操作,內(nèi)存就可以得到釋放。而沒有釋放,至少有一個沒做到。
- 循環(huán)緩沖池的作用是為了避免頻繁的、大量的內(nèi)存分配和釋放,特別是視頻幀數(shù)據(jù),一幀就上百k。同時也解釋了為什么內(nèi)存一點(diǎn)都沒有釋放,使用了池,要么全部釋放,要么一點(diǎn)都不釋放。
從內(nèi)部再回到外部,先檢查是否有frame沒有釋放。這時確實(shí)是有的,就在:
retval = avcodec_receive_frame(decoder->codecCtx, frame);
if (retval != 0) {
TFCheckRetval("avcodec receive frame");
av_frame_free(&frame);//漏掉了這里
continue;
}
在解碼失敗后,就直接continue
了。在意識里,好像這里的frame是無用的,沒數(shù)據(jù)的,所以就直接忽略了,接下一個。就死在了這里。
在把這種的frame都釋放時候,還是有問題,就剩下av_buffer_pool_uninit
這個了。這個函數(shù)的調(diào)用里用戶使用的外層很遠(yuǎn),最終查到是從avcodec_close
這里進(jìn)入的。在邏輯也是合理的,解碼結(jié)束了,才需要把分配的內(nèi)存銷毀。但是不要直接調(diào)用avcodec_close
,而是使用avcodec_free_context
,把codec相關(guān)的其他東西一并釋放了。
到這,終于內(nèi)存釋放了。重點(diǎn)在于認(rèn)識到有個pool的存在,這個在網(wǎng)上資料并不多。