目录
0 前言
在介绍字典操作api之前先介绍下字典的数据结构,由于是ffmpeg中的一个基础功能项,所以属于libavutil(lavu)库,头文件和源文件是dict.h和dict.c。
在头文件dict.h的开头部分有对ffmpeg中的该项功能做了简单的介绍,以及相应API使用的描述。
0.1 ffmpeg中字典类型的描述
/**
* @file
* Public dictionary API.
* @deprecated
* AVDictionary is provided for compatibility with libav. It is both in
* implementation as well as API inefficient. It does not scale and is
* extremely slow with large dictionaries.
* It is recommended that new code uses our tree container from tree.c/h
* where applicable, which uses AVL trees to achieve O(log n) performance.
*/
-
提供AVDictionary是为了兼容libav库。其APIs是效率低下的,无法扩展并且在大字典的情况下非常慢(由于是链表,其查询时间复杂度O(n))。
-
新代码建议使用树状容器,定义在tree.c和tree.h中,其使用AVL树来实现O(log n)的效率。
0.2 API使用简介
/**
* @addtogroup lavu_dict AVDictionary
* @ingroup lavu_data
*
* @brief Simple key:value store
*
* @{
* Dictionaries are used for storing key:value pairs. To create
* an AVDictionary, simply pass an address of a NULL pointer to
* av_dict_set(). NULL can be used as an empty dictionary wherever
* a pointer to an AVDictionary is required.
* Use av_dict_get() to retrieve an entry or iterate over all
* entries and finally av_dict_free() to free the dictionary
* and all its contents.
*
@code
AVDictionary *d = NULL; // "create" an empty dictionary
AVDictionaryEntry *t = NULL;
av_dict_set(&d, "foo", "bar", 0); // add an entry
char *k = av_strdup("key"); // if your strings are already allocated,
char *v = av_strdup("value"); // you can avoid copying them like this
av_dict_set(&d, k, v, AV_DICT_DONT_STRDUP_KEY | AV_DICT_DONT_STRDUP_VAL);
while (t = av_dict_get(d, "", t, AV_DICT_IGNORE_SUFFIX)) {
<....> // iterate over all entries in d
}
av_dict_free(&d);
@endcode
*/
-
AVDictionary用来存储键-值对;
-
av_dict_set()可以用来创建AVDictionary,通过给方法传入NULL指针,其内部会创建新的AVDictionary,并通过这个指针返回给用户新创建的AVDictionary对象;注意上面源码的示例,在key和value都已经分配好空间的时候,使用av_dict_set()时需要设置flags以便使得函数内部不再为key和value另分配空间了,直接利用外部已经分配好的。
-
av_dict_get() 可以用来获取AVDictionary的单个条目,进行迭代就可以获取AVDictionary的所有条目;
-
av_dict_free()用来释放AVDictionary所占用的所有空间。
1 Structs && Flags
1.1 AVDictionary
typedef struct AVDictionary AVDictionary; // libavutil/dict.h
struct AVDictionary { // libavutil/dict.c
int count;
AVDictionaryEntry *elems;
};
1.2 AVDictionaryEntry
typedef struct AVDictionaryEntry { // libavutil/dict.h
char *key;
char *value;
} AVDictionaryEntry;
1.3 Flags
对于以下7类flag,每个flag占据一个bit。代表的意思见如下源码中的英文释义
#define AV_DICT_MATCH_CASE 1 /**< Only get an entry with exact-case key match. Only relevant in av_dict_get(). */
#define AV_DICT_IGNORE_SUFFIX 2 /**< Return first entry in a dictionary whose first part corresponds to the search key,
ignoring the suffix of the found key string. Only relevant in av_dict_get(). */
#define AV_DICT_DONT_STRDUP_KEY 4 /**< Take ownership of a key that's been
allocated with av_malloc() or another memory allocation function. */
#define AV_DICT_DONT_STRDUP_VAL 8 /**< Take ownership of a value that's been
allocated with av_malloc() or another memory allocation function. */
#define AV_DICT_DONT_OVERWRITE 16 ///< Don't overwrite existing entries.
#define AV_DICT_APPEND 32 /**< If the entry already exists, append to it. Note that no
delimiter is added, the strings are simply concatenated. */
#define AV_DICT_MULTIKEY 64 /**< Allow to store several equal keys in the dictionary */
2 APIs
2.1 av_dict_count()
av_dict_count() 声明:返回dictionary中的条目数
/**
* Get number of entries in dictionary.
*
* @param m dictionary
* @return number of entries in dictionary
*/
int av_dict_count(const AVDictionary *m);
av_dict_count() 源码:
int av_dict_count(const AVDictionary *m)
{
return m ? m->count : 0;
}
2.2 av_dict_free()
av_dict_free() 声明:
- 声明:释放所有为AVDictionary结构体分配的空间,以及所有的keys和values所占用的空间
/** * Free all the memory allocated for an AVDictionary struct * and all keys and values. */ void av_dict_free(AVDictionary **m);
av_dict_free() 源码:
- 源文件:
void av_dict_free(AVDictionary **pm) { AVDictionary *m = *pm; if (m) { while (m->count--) { av_freep(&m->elems[m->count].key); av_freep(&m->elems[m->count].value); } av_freep(&m->elems); } av_freep(pm); }
-
一层层释放空间:迭代AVDictionary中的AVDictionaryEntry,不断释放key和value指向的空间 --> 释放AVDictionaryEntry列表空间 -->释放AVDictionary结构体空间。
-
注释释放空间时的av_freep()函数使用,传入的参数是个双重指针。详细分析见 FFMPEG4.1源码分析之 内存管理APIs av_freep() && av_free()
2.3 av_dict_copy()
av_dict_free() 声明:
- 声明:从一个AVDictionary复制条目到另外一个AVDictionary,注意以下几点
1)dst的类型是AVDictionary**,*dst为空的时候会创建一个AVDictionary,并通过dst返回
2)读取src的条目的时候使用AV_DICT_IGNORE_SUFFIX flag,以便可以遍历所有src的条目,在设置dst的条目的时候使用传入的flags/** * Copy entries from one AVDictionary struct into another. * @param dst pointer to a pointer to a AVDictionary struct. If *dst is NULL, * this function will allocate a struct for you and put it in *dst * @param src pointer to source AVDictionary struct * @param flags flags to use when setting entries in *dst * @note metadata is read using the AV_DICT_IGNORE_SUFFIX flag * @return 0 on success, negative AVERROR code on failure. If dst was allocated * by this function, callers should free the associated memory. */ int av_dict_copy(AVDictionary **dst, const AVDictionary *src, int flags);
av_dict_free() 源码:
int av_dict_copy(AVDictionary **dst, const AVDictionary *src, int flags)
{
AVDictionaryEntry *t = NULL;
while ((t = av_dict_get(src, "", t, AV_DICT_IGNORE_SUFFIX))) {
int ret = av_dict_set(dst, t->key, t->value, flags);
if (ret < 0)
return ret;
}
return 0;
}
- av_dict_get()不停迭代获取条目,注意flag为AV_DICT_IGNORE_SUFFIX,这个标志使得进行key匹配的时候,只要传入的key字符串与条目的key字符串前面的字符能匹配上,则认为该条目是要查找的条目。此处传入的key字符串为"",因此可以匹配所有的条目。
- av_dict_set() 将根据取出的每个条目,以及传入的flags值来设置目标AVDictionary。
2.4 av_dict_get()
av_dict_get() 声明:
/**
* 获取与key匹配的条目
* Get a dictionary entry with matching key.
*
* 返回的key和value值不要进行修改,否则会导致未定义的行为
* The returned entry key or value must not be changed, or it will
* cause undefined behavior.
*
* 为了迭代所有的条目,匹配key串设置为"",并且flags设置为AV_DICT_IGNORE_SUFFIX
* To iterate through all the dictionary entries, you can set the matching key
* to the null string "" and set the AV_DICT_IGNORE_SUFFIX flag.
*
* 入参prev指示查询将以prev为起始条目往下查询;当prev为NULL时,将从第一个条目起始进行查询
* @param prev Set to the previous matching element to find the next.
* If set to NULL the first matching element is returned.
*
* 入参key是进行匹配的字符串
* @param key matching key
*
* 入参flags将控制匹配条件,决定什么才算是key值匹配上了
* @param flags a collection of AV_DICT_* flags controlling how the entry is retrieved
*
* 返回值是满足匹配条件的条目,或者是NULL,如果没找到满足条件的条目的时候。
* @return found entry or NULL in case no matching entry was found in the dictionary
*/
AVDictionaryEntry *av_dict_get(const AVDictionary *m, const char *key,
const AVDictionaryEntry *prev, int flags);
av_dict_get() 源码:
源码比较简单,分析见注释。这儿主要把两个flag拿出来单独看下:
AV_DICT_MATCH_CASE标志表示key的匹配是大小写敏感的。
AV_DICT_IGNORE_SUFFIX标志表示key只要与条目中的key的前面的字符相同,那么就算是匹配上了。
AVDictionaryEntry *av_dict_get(const AVDictionary *m, const char *key,
const AVDictionaryEntry *prev, int flags)
{
unsigned int i, j;
// AVDictionary为空,自然条目为空
if (!m)
return NULL;
// 计算prev下一个entry是第几个,注意计算方式
// 若prev是不属于AVDictionary的条目或者已经是最后一个,
// 那么计算出来的i必然是越界的。
if (prev)
i = prev - m->elems + 1;
else
i = 0;
// 从第i个条目进行匹配
for (; i < m->count; i++) {
const char *s = m->elems[i].key;
// 大小写敏感,则不进行字符串转换
if (flags & AV_DICT_MATCH_CASE)
for (j = 0; s[j] == key[j] && key[j]; j++)
;
// 大小写不铭感,则都转换成大写字母,然后进行匹配
else
for (j = 0; av_toupper(s[j]) == av_toupper(key[j]) && key[j]; j++)
;
// 经过上述匹配过程,key中还有余留字符没有进行匹配,说明没有匹配上
// 因此进入下一个循环
if (key[j])
continue;
// 经过上述匹配过程,key中已无余留字符,但是条目的key中还有余留字符
// 若是AV_DICT_IGNORE_SUFFIX标志存在,那么也算是匹配上,否则,就是要求
// 全部都必须匹配上,那么也直接进入下一个循环
if (s[j] && !(flags & AV_DICT_IGNORE_SUFFIX))
continue;
// 上述条件都通过,那么当前条目就是满足匹配条件的条目,返回该条目
return &m->elems[i];
}
// 经过for循环还未从函数返回,那么没找到匹配的条目,此时返回NULL
return NULL;
}
2.5 av_dict_set()
av_dict_set() 声明:
/**
* 设置一个指定的AVDictionary中的条目,覆盖已存在的条目
* Set the given entry in *pm, overwriting an existing entry.
*
* 如果AV_DICT_DONT_STRDUP_KEY 或者AV_DICT_DONT_STRDUP_VAL被设置,那么
* 在出错的时候入参key和value所占用空间将被释放
* Note: If AV_DICT_DONT_STRDUP_KEY or AV_DICT_DONT_STRDUP_VAL is set,
* these arguments will be freed on error.
*
* 向字典添加新条目会使以前使用av_dict_get()返回的所有现有条目无效。
* Warning: Adding a new entry to a dictionary invalidates all existing entries
* previously returned with av_dict_get.
*
* pm是双重指针,若*pm为空,那么函数内部会分配一个新的AVDictionary结构,并将地址给*pm,
* 从而,调用方可以通过入参pm获取到分配的AVDictionary
* @param pm pointer to a pointer to a dictionary struct. If *pm is NULL
* a dictionary struct is allocated and put in *pm.
*
* @param key entry key to add to *pm (will either be av_strduped or added as a new key depending on flags)
* @param value entry value to add to *pm (will be av_strduped or added as a new key depending on flags).
* 传入NULL,将引发存在的条目被删除
* Passing a NULL value will cause an existing entry to be deleted.
* @return >= 0 on success otherwise an error code <0
*/
int av_dict_set(AVDictionary **pm, const char *key, const char *value, int flags);
av_dict_set() 源码:
int av_dict_set(AVDictionary **pm, const char *key, const char *value,
int flags)
{
AVDictionary *m = *pm;
AVDictionaryEntry *tag = NULL;
char *oldval = NULL, *copy_key = NULL, *copy_value = NULL;
// AV_DICT_MULTIKEY存在,表示AVDictionary的条目中允许出现重复的key值
// 如果不允许出现重复的key,那么获取传入key值所对应的条目
if (!(flags & AV_DICT_MULTIKEY)) {
tag = av_dict_get(m, key, NULL, flags);
}
// AV_DICT_DONT_STRDUP_KEY用于告知对于传入的key是否需要为其新分配空间
// 如果需要重新分配,则调用av_strdup来拷贝一份新的字符串,并将新串地址返给copy_key
// 如果不需要,则直接利用入参传入的key值所占据的空间
if (flags & AV_DICT_DONT_STRDUP_KEY)
copy_key = (void *)key;
else
copy_key = av_strdup(key);
// 同上
if (flags & AV_DICT_DONT_STRDUP_VAL)
copy_value = (void *)value;
else if (copy_key)
copy_value = av_strdup(value);
// 如果不存在AVDictionary,那么新创建一个
if (!m)
m = *pm = av_mallocz(sizeof(*m));
// 以下三种情况认为是出错:
// 1 如果m还为空,那边就是AVDictionary空间分配失败
// 2 如果传入的key不为空,但是内部copy_key为空,也即分配空间失败
// 3 如果传入的value不为空,但是内部copy_value为空,也即分配空间失败
if (!m || (key && !copy_key) || (value && !copy_value))
goto err_out; // 出现空间分配失败,跳转到err_out标签处理
// tag存在,也即不允许存在重复key的情况下,找到了传入key对应的已存在条目
// 那么就不需要创建新的条目了
if (tag) {
// AV_DICT_DONT_OVERWRITE表示不允许覆盖已有值
// 如果不允许重写value值,则直接释放掉copy_key和copy_value所占用的空间,并且返回0
if (flags & AV_DICT_DONT_OVERWRITE) {
av_free(copy_key);
av_free(copy_value);
return 0;
}
// AV_DICT_APPEND表示允许在当前value之后追加值
// 如果允许重写,允许在当前value后追加,那么使用oldval变量记住条目中的value值
// 如果允许重写,但是不许追加,那么释放掉条目中的value空间
if (flags & AV_DICT_APPEND)
oldval = tag->value;
else
av_free(tag->value);
av_free(tag->key); // 释放调用条目中key占用的空间
// 将最后一个条目内容复制到tag这个条目,这是干啥呀?
// 请看后文!!!并且注意啊,m->count在这个时候自减一了!!!!
*tag = m->elems[--m->count];
// 需要创建新条目的场景
} else if (copy_value) {
// 使用av_realloc()来扩展m->elems指向的空间,扩展一个AVDictionaryEntry槽位
AVDictionaryEntry *tmp = av_realloc(m->elems,
(m->count + 1) * sizeof(*m->elems));
// 分配失败,跳转到err_out标签处理
if (!tmp)
goto err_out;
// 由于av_realloc扩展后的地址可能与之前的不一致,因此
// 还需将m->elems指向分配后的空间
m->elems = tmp;
}
// copy_value不为空,意味着要进行设置
// 不论是新创建条目的场景还是设置旧有条目的场景,最后
// 一个条目都是目的条目!!注意设置旧有条目的场景的最后一个语句干啥了~~
if (copy_value) {
// 更新条目key && value指针
m->elems[m->count].key = copy_key;
m->elems[m->count].value = copy_value;
// 如果存在旧值,并且value是允许append,那么进行如下操作
if (oldval && flags & AV_DICT_APPEND) {
size_t len = strlen(oldval) + strlen(copy_value) + 1; // 计算旧值+新值总大小
char *newval = av_mallocz(len); // 为新旧二者创建新的空间
if (!newval) // 如果创建失败,跳转err_out标签进行处理
goto err_out;
// 将旧值和新值都拷贝到新分配的空间,并释放旧空间
av_strlcat(newval, oldval, len);
av_freep(&oldval);
av_strlcat(newval, copy_value, len);
m->elems[m->count].value = newval; // 更新条目的value指针
av_freep(©_value);
}
m->count++; // 条目数加1
// copy_value为空,意味着要删除条目,那为啥这儿只做了释放copy_key的操作?
// 奇怪呢~~为啥这儿删除条目,m->elems按理说应该减一才对
// 为啥不减呢?结合上面的代码认真思考
} else {
av_freep(©_key);
}
// 若删除到无条目的情况下,释放空间
if (!m->count) {
av_freep(&m->elems);
av_freep(pm);
}
// 返回成功
return 0;
err_out:
if (m && !m->count) {
av_freep(&m->elems);
av_freep(pm);
}
av_free(copy_key);
av_free(copy_value);
return AVERROR(ENOMEM);
}
主要的代码都进行了注释,此处将主要的逻辑进行阐述
- 代码最开始,需要对传入参数进行一系列的验证:
1.1) 验证AVDictionary结构体是否存在,若不存在则需要新建一个
1.2) 验证flags提供的属性:
1.2.1)根据入参flags相应标志位AV_DICT_MULTIKEY && AV_DICT_DONT_STRDUP_VAL 是否置位来决定是否需要新建空间,拷贝key和value的字符串
1.2.2)根据入参flags相应标志位AV_DICT_MULTIKEY是否置位来确定是操作新的条目,还是操作AVDictionary中已经存在的条目 - 根据是操作新建条目还是操作旧有条目进行区分处理,目标:其实就是针对这两种情况整理出一个统一的条目出来,以便后续操作能统一起来。
2.1)操作旧有条目:
2.1.1) 如果入参flags的AV_DICT_DONT_OVERWRITE被置位,那么意思就是不能重写旧条目,“既要操作旧条目,又不能重写旧条目”,那就没法操作了,所以释放分配key && value空间,函数退出。
2.1.2)如果入参flags的AV_DICT_APPEND置位,意思就是新value拼接到旧value之后,那么旧value还有用,否则没用了就释放旧value所占空间。
2.1.3)释放掉旧有key。还记否?flags中有两个标志位是控制key匹配条件的,一个是控制匹配时大小写是否敏感,一个控制是需要完全匹配还是只需前面的字符能匹配上即可。因此,注意key可是会被替换掉的,所以,应用上要根据场景正确的使用,否则,会造成不必要的困惑。
2.1.4)*tag = m->elems[--m->count]; 这句话是重点需要理解的地方,首先是一点是将最后一个条目内的值复制到需要操作的条目上,这儿的操作是为了让最后一个条目成为需要操作的旧有条目,与新分配条目达成一致,因为新分配条目也会附加到最后的一个条目!!!其次,m->count会自减一,那么最后一个条目会先从当前所有条目中除名,这个也是为了后续能统一操作!!!
2.2)操作新条目:
2.2.1)使用av_realloc()来扩展空间,增加一个条目结构体空间;
2.2.2)由于av_realloc()处理之后的空间地址不一定与原地址相同,因此还需要m->elems = tmp;
2.2.3)注意,此时并没有m->count加1,因此新增的条目结构体还未纳入AVDictionary的版图。 - 根据copy_value是否为NULL,来决定是进行条目的删除操作还是修改操作
3.1)copy_value为NULL,则是需要进行条目的删除。
3.3.1)旧有条目的情形:曾记否?旧有条目的最后一个在步骤2中已经提前除名了,因此,此处只需要释放必要的空间旧ok了,那就是将key值得字符串释放就行了
3.3.2)新建条目的情形:虽然已经新增了一个条目的空间,但是m->count并未加1,也就还未纳入AVDictionary的版图。因此,此处的操作跟3.3.1一致,但是注意这儿是否有内存泄漏?没有的,因为m->elems还指向这个分配的空间呢~
3.2)copy_value不为NULL的情况,那么就是重写条目咯~ 注意,在该情况下,对于m->elems进行了越界访问,由于步骤2中的处理,这儿就能统一处理了,这儿就不赘述了。当然对于重写条目时是进行append的情况进一步处理,见源码,此处也不再多做讲解。