先拋一個球,從一段代碼開始
AVPacket packet;
av_init_packet(&packet);
packet->data = NULL;
packet->size = 0;
while (av_read_frame(ifmt_ctx, &packet) >= 0) {
av_free_packet(&packet);
}
這段代碼一般也是經常用到了。那麼問題來了,爲什麼packet已經釋放了,av_read_frame裏面還能繼續使用這個packet。不知道大家有沒有想過這個問題。
先講個故事,假設有一個avpacket,另一個人想拷貝,它怎麼辦呢,再開闢一個空間,然後把avpacket的data copy給他?這太浪費內存了,因爲data裏面存的是視頻解壓縮數據,數據量通常都很大的,幾千字節到上萬字節不等,我們完全沒必要做這麼愚蠢的copy,因爲data是一個指針,我們完全可以讓新的avpacket的data也指向src avpacket data指針指向的數據區就可以了。但是這又引入了新的問題,就是這麼多人都指向了數據區的內存,萬一有人想釋放data空間了,他可不知道誰還要繼續使用這塊內存。所以data數據空間變成了燙手山芋,沒人敢釋放了。所以爲了解決這個問題,ffmpeg引入了引用計數。專門找個人來計數,每個人想釋放的時候,這個計數值refcount就-1,如果增加了新的人引用它,refcount就+1,如果某個時刻refcount等於0 了,就表示現在可以安全釋放了。
下面具體看ffmpeg怎麼實現的,先看AVPacket相關的結構體,我只展示和我們主題有關的東西
AVPacket{
/**
* A reference to the reference-counted buffer where the packet data is
* stored.
* May be NULL, then the packet data is not reference-counted.
*/
AVBufferRef *buf;
uint8_t *data;
int size;
}
AVBufferRef {
AVBuffer *buffer;
uint8_t *data;
int size;
}
AVBuffer {
uint8_t *data;
int size;
volatile int refcount;
}
buf這段註釋是什麼意思呢。意思是說buf指向引用計數緩衝區,如果buf爲NULL,就說明這個avpacket是無引用計數的,說白了,就是現在只要他一個人在使用data指向的數據空間。換個角度,buf不爲NULL,就說明這個avpacket是ref的,有引用計數的
我們再看av_init_packet()幹了什麼
pts,dts等一系列字段全部給初始化,buf=NULL,data無分配空間。說明剛開始出來,還是unref的 avpacket
av_read_frame先不看,看av_free_packet()。
這裏我們關心的主要做了兩件事,第一件事是如果pkt->buf不爲NULL,會調用av_buffer_unref, 第二件事是就是把data置爲NULL, 這裏注意,packet本身沒釋放。所以這個函數是內存泄漏隱患的。肯定內存泄漏的是avpacket結構體本身空間未釋放,其次因爲這裏只是把data置空,所以data可能內存泄漏,因爲是存在一種情況buf爲NULL,但是data分配了空間。比如看下面的代碼
AVPacket *packet=(AVPacket*)av_malloc(sizeof(AVPacket));
av_init_packet(packet);
packet->data=(uint8_t*)malloc(128);
packet->size=128;
av_free_packet(packet);
av_freep(&packet);//釋放packet並將其置空
這段代碼的packet的data 調用av_free_packet()就不會被釋放。需要我們手動釋放。那麼什麼時候data釋放,什麼時候data不釋放呢.這個問題下面慢慢講。
繼續分析av_free_packet,如果pkt->buf不爲空,那麼執行av_buffer_unref
分析這個代碼前,先看下我上面列的AVPacket相關的結構體。文字描述就是,
avpacket裏面有一個buf,有一個data.
這個buf又有一個buffer,有一個data.
這個buffer也有一個data.
我們注意到avpacket裏面有data,buf裏面有data,buffer裏面也有data,其實這三個data都是同一個data,
即avpacket->data=buf->data=buffer->data。每個結構體都存了一份data的地址,實際是方便釋放的時候,知道數據空間的首地址.
回過頭看av_buffer_unref();
首先要滿足buf不爲空,如果buf爲空,直接滾出,所以不存在還有釋放data這回事。所以上面那個內存泄漏就是這麼來的。
假設我們buf不爲空,繼續分析。
b=(*dst)->buffer. 這裏dst就是packet的buf,這句話就是先把buffer的地址暫存起來,
src爲NULL,所以走av_freep(dst);這句話就是釋放了packet的buf,注意啊,av_freep實際就是free+置空。所以這裏只是釋放結構體本身的內存空間,裏面的data堆空間是不會被處理的。
所以我們在釋放這個結構體之前,剛剛就是提前把他裏面的指針地址給取出來。
繼續分析,執行avpriv_ato...()函數,這裏就是將buffer的refcount減1,然後值返回,如果refcount變成了0,
通過b->free函數釋放data數據空間,然後釋放buffer
所以我們看到真正釋放data,不是直接free的avpacket裏面的那個data,而是曲曲折折free的是avpacket->buf->bufer->data
這個層級關係中,只有有誰是空的,data都不會被釋放。
總結:av_free_packet(packet) 不能釋放packet本身的內存空間,所以需要手動調用av_freep(&packet);
注意,av_freep需要傳指針的地址
其次,av_free_packet能夠自動釋放data,取決於需要同時滿足packet的ref不爲NULL且引用計數變爲0
再說個有些人會搞混的,就是packet->data是否爲空,和data指向的堆空間是否被釋放,是完全兩碼事,不要搞錯了啊。
data本身是一個棧內存上的指針變量,data你不手動把它置爲NULL, 就算你free了data, data也不會變成NULL的。free的是它指向的那塊內存,對它自己是不會有影響的。
還有一個問題,就是你把data置爲NULL,不是說free它了。這個c和java不同,java裏面的虛擬機是根據內存引用計數來的,沒有引用了,垃圾回收器就回收了。但是c裏面,沒有垃圾回收器自動回收這麼一說。每塊堆內存都需要我們自己去管理釋放。
data沒有調用free(),就直接置爲NULL,那是妥妥的內存泄漏。free()的本質就是告訴系統,這塊內存我現在不用了,請標記爲未分配狀態。相當於free()是一個通知系統的操作。你不free(),直接置NULL, 在系統那,那塊堆內存就一直是分配狀態,無法得到回收。
av_read_frame代碼很複雜,我們採用debug方式觀察執行完av_read_frame以後,buf是不是爲NULL,
結論是不爲空,說明av_read_frame會生產一個ref的avpacket, (且內部調用了av_init_packet)。
此時refcount=1,所以free的時候,滿足refcount-1=0,能夠釋放掉data空間。
下面繼續分析增加一個data引用,內存中會發生什麼事。
涉及到的函數是av_packet_ref()
...
while (av_read_frame(format_context, &packet) >= 0) {
AVPacket dst;
av_packet_ref(&dst, &packet);
av_free_packet(&packet);
}
av_packet_copy_props,把一些pts,dts等字段進行copy
判斷src是不是ref的,如果src不是ref的,先給dst的buf分配內存空間,然後把src的data數據拷貝給它。
注意,此時dst的data和src的data是兩塊內存。
如果src是ref的,執行av_buffer_ref
給dst的buf分配內存空間(即這裏的ret),並且把src的buf的值賦給它。
這裏*ret=*buf,是關鍵。
一個指針p1把值賦給另一個指針p2,如果賦值內容是指針,就copy指針地址,如果是基礎數據類型,比如int,字符串類型,就copy值。
此時buf中的內容是AVBuffer,data,size. 前兩個都是指針。所以是copy指針地址,即av_buffer_ref實際上兩者共享同一個data堆空間。且只要buf釋放自己的data,則ret的data也會被釋放。但是buf改變自己的size的值,不會影響ret的size的值。(string類型或者int類型的數據不會受影響)
繼續分析代碼,執行到avpriv_atomic_int_add_and_fetch ,這裏是把buf->buffer->refcount++了, 因爲refcount屬於buffer指針的,和dst是共享的一份,所以等同於dst的refcount++了。
這樣就能保證,每個人在操作引用計數的時候,都操作的是同一個。
和av_packet_ref對應的函數,是av_packet_unref
這裏av_buffer_unref 也都已經分析過。