av_opt_copy() 声明:
所属库:libavutil(lavu),注意是lavu库,这个是ffmpeg中的功能库,代表的是一个功能头文件:libavutil/opt.h声明:从src对象中拷贝选项到dest对象中。 1)要点一:注意理解“拷贝选项”的意思是将src对象中与src->av_class->option能对应上的那些成员,拷贝一份值到目的dest对象对应的成员。 2)要点二:正如声明上的说明,dest对象的原有字段所占用的内存将被释放掉,然后重新申请内存,并拷贝src对象对应成员的值到新申请的内存上——除非src和des的成员都指向同样的内存,这种情况下不会申请新的内存。 /** * Copy options from src object into dest object. * * Options that require memory allocation (e.g. string or binary) are malloc'ed in dest object. * Original memory allocated for such options is freed unless both src and dest options points to the same memory. * * @param dest Object to copy from * @param src Object to copy into * @return 0 on success, negative on error */ int av_opt_copy(void *dest, const void *src);
av_opt_copy() 源码:
源文件:libutil/opt.c int av_opt_copy(void *dst, const void *src) { const AVOption *o = NULL; const AVClass *c; int ret = 0; // 验证参数有效性 if (!src) return AVERROR(EINVAL); c = *(AVClass **)src; if (!c || c != *(AVClass **)dst) return AVERROR(EINVAL); // 循环获取src的选项,然后将选项对应的src成员值拷贝到dest成员 while ((o = av_opt_next(src, o))) { void *field_dst = (uint8_t *)dst + o->offset; // 指向目标成员的指针 void *field_src = (uint8_t *)src + o->offset; // 指向源成员的指针 uint8_t **field_dst8 = (uint8_t **)field_dst; // 指向目标成员的指针(注意类型不同) uint8_t **field_src8 = (uint8_t **)field_src; // 指向原成员的指针(注意类型不同) // 字符串类型 if (o->type == AV_OPT_TYPE_STRING) { // 判断源串与目的串是否是同一个,如果不是,则释放目的串空间 // 如果是的话就不能释放空间了,否则源串也没了,源串指针就变成悬空指针了 if (*field_dst8 != *field_src8) av_freep(field_dst8); // 分配空间并拷贝源串到目的串 *field_dst8 = av_strdup(*field_src8); // if (*field_src8 && !*field_dst8) ret = AVERROR(ENOMEM); // 二进制数 } else if (o->type == AV_OPT_TYPE_BINARY) { // 求取二进制块的大小 int len = *(int *)(field_src8 + 1); // 判断源二进制块与目的二进制块是否是同一块 // 若不是则释放当前目的块的空间,若是则不能释放,否则源块也跟着没了。 if (*field_dst8 != *field_src8) av_freep(field_dst8); // 复制原块到目的块 *field_dst8 = av_memdup(*field_src8, len); // 判断目的二进制块是否分配并拷贝成功, // 若不成功注意需要使得len也为0,保证二者的一致性 if (len && !*field_dst8) { ret = AVERROR(ENOMEM); len = 0; } // 最后设置二进制块的大小值 *(int *)(field_dst8 + 1) = len; // const常量 } else if (o->type == AV_OPT_TYPE_CONST) { // do nothing // 字典类型AVDictionary* } else if (o->type == AV_OPT_TYPE_DICT) { // 转换地址类型 AVDictionary **sdict = (AVDictionary **) field_src; AVDictionary **ddict = (AVDictionary **) field_dst; // 判断源AVDictionary与目的AVDictionary是否是同一个 // 若是同一个则不能释放空间,否则源AVDictionary也没了 // 若是不是同一个,则使用av_dict_free()来释放当前目的AVDictionary的空间 if (*sdict != *ddict) av_dict_free(ddict); *ddict = NULL; // 进行字典拷贝 av_dict_copy(ddict, *sdict, 0); // 若源AVDictionary的条目数不等于目的AVDictionary的条目数 // 内存空间分配失败 if (av_dict_count(*sdict) != av_dict_count(*ddict)) ret = AVERROR(ENOMEM); } else { // 其他类型 int size = opt_size(o->type); if (size < 0) ret = size; else memcpy(field_dst, field_src, size); } } return ret; }验证入参的有效性:确保3点, 1) 源对象src不为空; 2) 源对象src的第一个参数为AVClass*; 3) 源对象src和目的对象dest的第一个参数AVClass*指向的是同一个对象AVClass (如此进行AVOption拷贝才是有效的)
循环调用av_opt_next()取出src的选项,并将选型所对应的src成员值赋给dest的对应的成员:av_opt_next()函数的详解见文章FFMPEG4.1源码分析之 av_opt_next()。注意本函数对于不同类型的成员,赋值的方式是不一样的,在数据拷贝的时候对字符串类型,二进制类型,常量类型,字典类型进行特例处理,剩余的其他类型统一处理。在跟随下面分析过程中,认真思考下,为什么这几个类型会作为特例处理,他们的共性是什么?首先看看有哪些类型定义:
enum AVOptionType{ AV_OPT_TYPE_FLAGS, AV_OPT_TYPE_INT, AV_OPT_TYPE_INT64, AV_OPT_TYPE_DOUBLE, AV_OPT_TYPE_FLOAT, AV_OPT_TYPE_STRING, AV_OPT_TYPE_RATIONAL, AV_OPT_TYPE_BINARY, ///< offset must point to a pointer immediately followed by an int for the length AV_OPT_TYPE_DICT, AV_OPT_TYPE_UINT64, AV_OPT_TYPE_CONST, AV_OPT_TYPE_IMAGE_SIZE, ///< offset must point to two consecutive integers AV_OPT_TYPE_PIXEL_FMT, AV_OPT_TYPE_SAMPLE_FMT, AV_OPT_TYPE_VIDEO_RATE, ///< offset must point to AVRational AV_OPT_TYPE_DURATION, AV_OPT_TYPE_COLOR, AV_OPT_TYPE_CHANNEL_LAYOUT, AV_OPT_TYPE_BOOL, };1)字符串类型(AV_OPT_TYPE_STRING):字符串类型成员是char*类型,此处假设成员名为m_str。非常重要的一点是弄清楚field_dst8 和 field_src8在此种条件下指代的是什么。 1.1)首先,field_dst变量是一个指针,其指向了当前要处理的目标成员地址m_str,即field_dst指向了m_str;field_dst8是个double point(双重指针),是对field_dst进行强制转换的结果,也就是说field_dst8变量的值其实和field_dst变量的值是一样的,field_dst8存储的是目标成员的地址!!!不过由于类型不同,所指代的意义不一样。 1.2)其次,if (*field_dst8 != *field_src8) 这个判断代表的含义需要重点理解下,field_dst8为目标成员地址,那么*field_dst8当然是取出目标成员值咯,由于目标成员m_str也是一个指针,其值是指向一个字符串的,那么*field_dst8内容的值是个指向字符串的地址,并且需要按照uint8_t *的类型来解释这个值(field_dst8是uint8_t **类型,*field_dst8类型当然是uint8_t *了)。结论:*field_dst8就是一个指针,指向目标字符串成员变量所指向的同一个字符串;同理,*field_src8指向源字符串成员指向的同一个字符串;上述if语句的含义就是看源成员和目的成员是否是指向同一个字符串。
1.3)再次,若二者不指向同一个字符串,那么使用av_freep()释放掉目的成员所占用的字符串内存。相等则不能释放空间,否则源串也跟着没了,并且源串指针变成了悬空指针。av_freep()函数的具体解析见 FFMPEG4.1源码分析之 内存管理APIs av_freep() && av_free() 1.4)最后,使用av_strdup()从源成员拷贝一份到目的成员地址。av_strdup()解释详见 FFMPEG4.1源码分析之 内存管理APIs av_strdup() && av_strndup() 1.5)做最后的验证,若源串不为空,而av_strdup()得到的目的串为空,那么返回错误码AVERROR(ENOMEM),告知内存空间不足。 2)二进制数据块类型(AV_OPT_TYPE_BINARY):二进制成员由指向二进制块uint8*类型的成员变量(假设为uint8* m_pBinary)+ 块长度成员变量(假设为int m_nBinary)构成。 2.1) 首先:int len = *(int *)(field_src8 + 1);计算二进制数据块的大小,field_src8指向二进制成员变量的首地址,也即指向m_pBinary,而field_src8 + 1则指向了m_nBinary,此时做类型强制转换成int*,然后取值,显然得到的就是m_nBinary的值,即二进制块的大小; 2.2) 其次:if (*field_dst8 != *field_src8);基于字符串类型的分析,此处就只说结论了,*field_dst8是当前目标二进制块地址,*field_src8是当前源二进制块的地址,显然此处比较的是二者是否是同一个二进制块。如果不等则不是,那么先使用av_freep()来释放目标二进制块的内存空间,如果相等,则不能释放空间,否则源二进制块也没了。 2.3)接着,使用av_memdup()进行内存分配和拷贝,从源二进制块拷贝一份到新分配的目的二进制块空间。 2.4)验证len不为0,2.3)操作是否成功,若不成功,那么记得将len置为0,保证m_pBinary和m_nBinary保持一致性 2.5)最后记得设置m_nBinary为最终的真实的长度len。 3)常量类型(AV_OPT_TYPE_CONST):这个就不多说了,常量不可变,无法赋值。 4)字典类型(AV_OPT_TYPE_DICT):基于字符串类型和二进制块类型已经进行了消息的解析,字典类型其实也是类似的,源码上已经进行了注释,这儿就不再多赘述,关于源码中对AVDictionary类型进行的相关操作av_dict_free(),av_dict_copy(),av_dict_count()详见 FFMPEG4.1源码分析之 字典类型APIs av_dict_free() && av_dict_copy() && av_dict_count() 5)其他类型:opt_size求出类型的大小之后,直接进行内存拷贝来覆盖目的地址的原值。
opt_size() 声明:
所属库:libavutil(lavu)头文件:无,静态函数源文件:libavutil/opt.c 需要注意的以下几点: 1)从二进制块类型的成员求取大小方式:可以确认该成员由两部分组成uint8_t*指向二进制块的指针 + int二进制块占字节数;二进制块类型成员size是指针所占字节数(一般是4)+ int类型所占字节数(一般也是4) = 一般是8; 2)AVOptionType有19种类型,而opt_size只求取了17种类型的值,对于const类型和AVDictionary*类型的成员没有求值。 2.1) const类型具体可以是任意数据类型,所以无法求取size; 2.2) AVDictionary*类型为什么不像字符串类型char* 以及 二进制块类型一样,当作指针处理来求取size呢?这个是我非常疑惑的一点 static int opt_size(enum AVOptionType type) { switch(type) { case AV_OPT_TYPE_BOOL: case AV_OPT_TYPE_INT: case AV_OPT_TYPE_FLAGS: return sizeof(int); case AV_OPT_TYPE_DURATION: case AV_OPT_TYPE_CHANNEL_LAYOUT: case AV_OPT_TYPE_INT64: case AV_OPT_TYPE_UINT64: return sizeof(int64_t); case AV_OPT_TYPE_DOUBLE: return sizeof(double); case AV_OPT_TYPE_FLOAT: return sizeof(float); case AV_OPT_TYPE_STRING: return sizeof(uint8_t*); case AV_OPT_TYPE_VIDEO_RATE: case AV_OPT_TYPE_RATIONAL: return sizeof(AVRational); case AV_OPT_TYPE_BINARY: return sizeof(uint8_t*) + sizeof(int); case AV_OPT_TYPE_IMAGE_SIZE: return sizeof(int[2]); case AV_OPT_TYPE_PIXEL_FMT: return sizeof(enum AVPixelFormat); case AV_OPT_TYPE_SAMPLE_FMT: return sizeof(enum AVSampleFormat); case AV_OPT_TYPE_COLOR: return 4; } return AVERROR(EINVAL); }
由上述源码的分析,可以看出特例处理中的4个类型除了常量类型不需要进行操作之外,其他三个特例的处理基本使相似的,其实也是因为这三类成员变量具体同一个特征:即均是指针类型的变量或含有指针,使得数据的内存并非都和结构体对象在一起,造成拷贝的时候需要进行深拷贝。就如同c++类中实现复制拷贝函数一样,必须进行深拷贝。