在寫這個播放器的時候,遇到了一些內存管理的問題,雖然棘手可是也讓我對此有了比較完善的理解,並且不少相關資料並無跟隨FFmpeg的更新,好比緩衝池AVBufferPool
的使用。ios
使用ffmpeg版本是3.4git
對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_packet
和avcodec_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
對此有兩種方案:測試
av_frame_ref
來接收,而不是直接的賦值。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
,這個函數有一個系列,就是進行原子性的加減乘除的,這個函數是先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這個名字還有上面那兩行註釋,以及個人測試能夠得出:
av_buffer_pool_init
)的時候,引用計數爲初始值1,調用av_buffer_pool_uninit
標記爲可銷燬,引用計數減1,這二者恰好匹配。從內部再回到外部,先檢查是否有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的存在,這個在網上資料並很少。