ffmpeg簡單分析系列–內存管理(AVBuffer)
- 本文基於ffmpeg4.2進行說明
- libavutil/buffer.h此頭文件主要是ffmpeg緩存數據的主要接口
- ffmpeg的內存管理的核心主要包含以下幾個數據結構:AVBuffer,AVBufferRef,BufferPoolEntry ,AVBufferPool
- 其中AVBuffer是最基礎也是最核心的,用於存放真正的數據以及釋放數據
- 但AVBuffer對外不公開,要通過AVBufferRef來間接使用AVBuffer
- BufferPoolEntry可以說是一個鏈表,是配合緩衝池AVBufferPool 的一個重要的數據結構
- 當完全弄懂AVBufferPool之後就會驚歎其設計的簡約而不簡單的巧妙構造
AVBuffer
- AVBuffer位於libavutil\buffer_internal.h,是ffmpeg內部接口,非對外公開,其定義如下
struct AVBuffer {
uint8_t *data; /**< data described by this buffer */
int size; /**< size of data in bytes */
/**
* number of existing AVBufferRef instances referring to this buffer
*/
atomic_uint refcount;
/**
* a callback for freeing the data
*/
void (*free)(void *opaque, uint8_t *data);
/**
* an opaque pointer, to be used by the freeing callback
*/
void *opaque;
/**
* A combination of BUFFER_FLAG_*
*/
int flags;
};
- data和size就不用多說了,就是實際存放數據的指針和大小;
- refcount是引用計數
- 中間的回調函數是用於釋放數據的,就是當refcount變爲0時,就會回調這個函數去釋放佔用的內存,ffmepg默認使用av_buffer_default_free這個,其內部使用av_free來釋放內存
- opaque就是用戶自定義的需要傳遞給回調函數的(對應回調函數的opaque參數);
- flags標識一些屬性,主要有BUFFER_FLAG_READONLY和BUFFER_FLAG_REALLOCATABLE兩個,BUFFER_FLAG_READONLY表示只讀,當引用計數爲1時表示只有一個對象引用它,此時它是可寫的,否則它就是隻讀的
- AVBuffer是FFMPEG內存管理的基石,深入理解AVBuffer的實現機制有助於後續FFMPEG源碼的研讀
- 既然AVBuffer是內部的,我們無法直接使用它,那麼通過誰來間接使用它呢,答案就是AVBufferRef
AVBufferRef
/**
* A reference to a data buffer.
*
* The size of this struct is not a part of the public ABI and it is not meant
* to be allocated directly.
*/
typedef struct AVBufferRef {
AVBuffer *buffer;
/**
* The data buffer. It is considered writable if and only if
* this is the only reference to the buffer, in which case
* av_buffer_is_writable() returns 1.
*/
uint8_t *data;
/**
* Size of data in bytes.
*/
int size;
} AVBufferRef;
- 以上的AVBufferRef.data和size與AVBuffer.data和size是一樣的
- 有兩種方式創建AVBufferRef,一種是通過av_buffer_alloc,只要指定大小即可;另一種是通過av_buffer_create,這種主要是用於已經有了數據的情況,同時它也支持自定義釋放此數據內存的方法
- AVBuffer在創建的時候引用計數爲1,當調用av_buffer_ref()對其進行操作時,引用計數+1,當av_buffer_unref對其操作時則引用計數-1(當-1後引用計數爲0時,av_buffer_unref將自動釋放分配的數據緩存
- 當AVBufferRef指向的AVBuffer的引用計數爲1時,它是可寫的,否則就是隻讀的,然而我們無法訪問AVBuffer,因此可以通過av_buffer_is_writable判斷
AVBufferRef指向的AVBuffer是否可寫,通過av_buffer_get_ref_count知道AVBufferRef指向的AVBuffer的引用計數等等,更多的api都在libavutil/buffer.h中
AVBufferPool
typedef struct BufferPoolEntry {
uint8_t *data;
/*
* Backups of the original opaque/free of the AVBuffer corresponding to
* data. They will be used to free the buffer when the pool is freed.
*/
void *opaque;
void (*free)(void *opaque, uint8_t *data);
AVBufferPool *pool;
struct BufferPoolEntry *next;
} BufferPoolEntry;
struct AVBufferPool {
AVMutex mutex;
BufferPoolEntry *pool;
/*
* This is used to track when the pool is to be freed.
* The pointer to the pool itself held by the caller is considered to
* be one reference. Each buffer requested by the caller increases refcount
* by one, returning the buffer to the pool decreases it by one.
* refcount reaches zero when the buffer has been uninited AND all the
* buffers have been released, then it's safe to free the pool and all
* the buffers in it.
*/
atomic_uint refcount;
int size;
void *opaque;
AVBufferRef* (*alloc)(int size);
AVBufferRef* (*alloc2)(void *opaque, int size);
void (*pool_free)(void *opaque);
};
- 以上的BufferPoolEntry的opaque、data、free都是指向了AVBuffer最開始的opaque、data、free,然後AVBuffer自己的opaque和free則指向了BufferPoolEntry和pool_release_buffer
- 爲何這樣設計呢,因爲AVBuffer最終要進行釋放的話,那還是得調用它自己最本來的free函數,但是此時由於要放到緩存池管理,因此在free時不能真正把AVBuffer釋放了,因此用BufferPoolEntry來保存AVBuffer真正的釋放內存的函數,然後再用緩存池釋放函數pool_release_buffer來代替free函數,這樣,當用戶釋放的時候,AVBuffer的free函數已經指向了pool_release_buffer函數,因此可以在pool_release_buffer裏把AVBuffer返回給緩存池,等到緩存池自己想要被釋放的時候,這個時候緩存池就從BufferPoolEntry把之前保存的真正釋放AVBuffer的函數取出來,進行調用;不得不說,這個設計雖然非常繞,但確實精妙
- AVBufferPool.size指的不是這個緩衝池有多少個AVBuffer,而是指AVBuffer.size
- AVBufferPool在初始化時AVBufferPool.pool是空的
- av_buffer_pool_get()會判斷是否有AVBufferPool.pool,有則取出使用後AVBufferPool.pool指向下一個,無則創建一個新的BufferPoolEntry,BufferPoolEntry.pool都會指向創建他的AVBufferPool
- 這裏有點繞,其實就是AVBufferPool指向了BufferPoolEntry鏈表的頭,這樣AVBufferPool就能管理整條鏈了,而每個BufferPoolEntry都指向了AVBufferPool,也就是說每個BufferPoolEntry都能找到他的管理者AVBufferPool
- pool_alloc_buffer() 就是創建一個BufferPoolEntry並指向AVBufferPool,並利用AVBufferPool創建一個AVBufferRef,然後BufferPoolEntry.data指向這個剛創建的AVBufferRef.buffer.data並返回這個AVBufferRef
- av_buffer_pool_init()時會將AVBufferPool.refcount置爲1
- av_buffer_pool_uninit()會將AVBufferPool.refcount減1,減1後如果等於1,那就會調用buffer_pool_free()進行釋放
- buffer_pool_free()內部會找出AVBufferPool的整個鏈條,從鏈頭開始進行逐一釋放內存,最後將自身也釋放並置null
- 從圖中可以看出,每個BufferPoolEntry都保存了指向AVBufferPool的指針
- BufferPoolEntry又是一個鏈表,都保存了指向下一個BufferPoolEntry的地址
- 而AVBufferPool則保存了指向BufferPoolEntry鏈表的表頭
- BufferPoolEntry雖然沒有保存AVBufferRef,但通過AVBufferRef間接保存了AVBuffer的data,free(),opaque,然後AVBuffer自己的opaque轉而指向BufferPoolEntry,free則轉而指向pool_release_buffer()這個函數
- 這裏要理解的就是AVBuffer和BufferPoolEntry的數據其實各自做了一定程度的交換,這裏的設計非常巧妙,一時搞不懂建議自己再仔細推敲一下
- AVBuffer本來的默認釋放函數是av_buffer_default_free(),這個函數是真正釋放內存的
- 通過AVBufferPool之後,AVBuffer本來的av_buffer_default_free()會被保存到BufferPoolEntry裏,然後被替換爲pool_release_buffer(),這樣當我們調用AVBuffer的free()函數時候,實際上他調到的是pool_release_buffer(),pool_release_buffer()會重新利用AVBuffer,將其放入緩存池繼續等待使用,只有當緩衝池要被釋放的時候,緩存池AVBufferPool由於保存了BufferPoolEntry,這個BufferPoolEntry是整個鏈表的頭,通過這個頭,其他BufferPoolEntry也就能遍歷出來了,而前面說過BufferPoolEntry保存了AVBuffer的真正釋放內存的函數av_buffer_default_free()的指針,通過它真正釋放內存
- 普通的AVBuffer平時釋放通過av_buffer_default_free即可,但是交給AVBufferPool後,釋放就交給pool_release_buffer函數了
- AVBufferPool存放了鏈表的頭指針,比如鏈表爲3->2->1,那麼AVBufferPool就指向了表頭3,當需要的時候就從中取出這個鏈表的頭3,然後AVBufferPool指向2,此時AVBufferPool裏的AVBufferPool鏈表就變成2->1了,當3使用完畢,進行釋放的時候,此時並不會進行真正的內存釋放,而是巧妙地利用了AVBuffer結構的釋放函數,將3重新接回表頭,變成3->2->1
- 不得不說AVBuffer的釋放函數設計得非常精巧,如果沒有加入緩存池,AVBuffer的釋放就是真正的內存釋放,如果加入緩存池,釋放後又可以回到緩存池繼續等待他人使用(此時AVBuffer內存還在)
參考
- 深入理解FFMPEG-AVBuffer/AVBufferRef/AVBufferPool_移動開發_muyuyuzhong的專欄-CSDN博客 https://blog.csdn.net/muyuyuzhong/article/details/79381152?utm_source=blogxgwz2
- FFmpeg視頻播放的內存管理 - 簡書 https://www.jianshu.com/p/9f45d283d904