FFmpeg視頻播放的內(nèi)存管理

在寫這個播放器的時候,遇到了一些內(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_packetavcodec_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é)省資源,然后又沒注意到這個注釋的話,很可能就會這么做。

對此有兩種方案:

  1. 繼續(xù)只使用一個frame來接收,但是在傳遞給下一步(渲染、播放等)的時候,下一步的模塊使用一個新的frame和av_frame_ref來接收,而不是直接的賦值。
  2. 每次解碼前構(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ù)是先fetchadd,先查詢再增加,所以返回的值是修改之前的。

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_refav_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)上資料并不多。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容