使用宏来提高代码可读性(代码的美感)
例1.宏CHARACTER_CLASS
定义
tokenizer.cc
文件中,需要判断某个character是属于哪种类型的字符,通过宏CHARACTER_CLASS
来定义字符类型,并且定义static类型的InClass()
接口来判断。
1 |
|
使用
使用时可以直接使用InClass():
1 | template<typename CharacterClass> |
例2.宏 PROTOBUF_DEFINE_ACCESSOR
FieldDescriptor
类,因为可能类型是多样的,在实现对外暴露default数据的函数时,为了提高代码可读性,使用了如下宏的方式(文件descriptor.cc中):
定义
1 | // These macros makes this repetitive code more readable. |
使用
1 | PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, default_value_int32 , int32 ) |
例3.宏BUILD_ARRAY
定义
BUILD_ARRAY宏定义如下,这里的INPUT是proto;OUTPUT是proto对应的descriptor;NAME是需要完成创建的成员;METHOD是创建descriptor成员时需要调用的函数;PARENT是发生嵌套时的上一级。
1 | // A common pattern: We want to convert a repeated field in the descriptor |
使用
DescriptorBuilder::BuildFile()中,利用FileDescriptorProto& proto来构建对应的descriptor:
1 | // Convert children. |
说明
各个Descriptor类中,使用count + 连续内存来保存成员,例如:
1 | class LIBPROTOBUF_EXPORT FileDescriptor { |
资源分配/处理的lazy机制
例1.类DescriptorPool数据分层设计
DescriptorPool的数据管理分为了多层(忽略了仅在protobuf内部使用 && 不推荐使用的underlay一层):
- 最顶层:
DescriptorPool::Tables tables_
,保存name->descriptor
; - 最底层:
DescriptorDatabase* fallback_database_
,保存name->file_descriptor_proto
(而不是直接的file_descriptor
)
查找时,如果第一层tables_
没找到,最终会到fallback_database_
中找对应proto,并且调用临时构造的DescriptorBuilder::Build*()
系列接口把生成的descriptor添加到tables_
中,然后再从tables_
中找。
这样数据分层设计的目的是:
- 用于定制地(on-demand)从某种”大”的database加载产生DescriptorPool。因为database太大,逐个调用
DescriptorPool::BuildFile()
来处理原database中的每一个proto文件是低效的。
为了提升效率,使用DescriptorPool来封装DescriptorDatabase,并且只建立正真需要的descriptor。 - 针对编译依赖的每个proto文件,并不是在进程启动时,直接构建出proto中所包含的所有descriptor,而是hang on,直到某个descriptor真的被需要:
(1)用户调用例如descriptor(), GetDescriptor(), GetReflection()的方法,需要返回descriptor;
(2)用户从DescriptorPool::generated_pool()
中查找descriptor;
可以看到descriptor的构建是hang-on的,只有需要使用某个descriptor时,才构建。适合依赖了很多的proto文件,但仅仅使用其中的少数proto的场景。
例2.类GeneratedMessageFactory映射关系加载
GeneratedMessageFactory
类,管理的从Descriptor* -> Message*
映射关系,并不是一开始就注册好的。仅仅在需要从descriptr查找message时(调用GeneratedMessageFactory::GetPrototype()
),才会:
- 通过file_name找到注册函数;
- 调用注册函数,完成
Descriptor* -> Message*
映射关系的注册; - 从
hash_map<const Descriptor*, const Message*> type_map_
查找到对应Message*
返回;
资源管理/内存复用
类RepeatedPtrFieldBase
RepeatedPtrFields的父类(不是模板类,提供了多个模板函数),本身保存/管理的数据类型为void*
(message对象的实际地址,也是通过连续内存array来保存)。
因为array中保存的是同一个descriptor对应的message,只是各个message中所包含的数据不一样,为了节省下message对象分配/释放的成本
,所以message可以被clear(clear操作会将primitive类型的field设置为0,其余类型field调用自身的clear()接口处理,类似std::string::clear()
,只清理数据并不回收内存)。
然后保留原有的内存地址在array中。下次需要从array中分配message时,优先使用这一批被clear的message(实现在RepeatedPtrFieldBase::AddFromCleared()
,参考GeneratedMessageReflection::AddMessage()
中的调用方式)。
为了管理cleared状态的message指针,引入了多个游标来标记数据:
current_size_
: 当前待处理的message地址;allocated_size_
:已经分配message的数据,current_size_ <= allocated_size_
,从current_size_
到allocated_size_
之间的message就是被cleared的;total_size_
:elements_[]
的长度,但从allocated_size_
到total_size_
之间的void*
是无效的,并没有指向任何message;
对应内存分布如下图所示:
封装多种类型,统一对外的服务
针对数据/行为简单的类型,使用轻量级的方案(struct/enum/union/switch-case
),来实现类型的封装,而不是采用继承方式来实现。
Symbol可能有多种类型,enum Type
表示具体类型,union
让多种类型都复用同一个内存地址:
1 | struct Symbol { |
宏CONSTRUCTOR
帮助提高代码可读性,来实现不同类型Symbol的构造函数:
1 |
|
宏CONSTRUCTOR
的使用:
1 | CONSTRUCTOR(Descriptor , MESSAGE , descriptor ) |
具体应用时,根据type
来区分处理:
1 | const FileDescriptor* GetFile() const { |
不同的类作为模版参数时,提供类独有的类型
类GenericTypeHandler
和类StringTypeHandler
需要作为模版类型参数(typehandler
),在子类RepeatedPtrField
在调用父类RepeatedPtrFieldBase
的模板函数时,通过模板参数直接传入父类RepeatedPtrFieldBase
,这里需要根据不同的typehandler
,返回对应不同的类型:
1 | template <typename TypeHandler> |
所以有如下方式,在不同模版参数类型中通过typedef
方式来实现类型名称的统一,因为对于模版来说,关键点就是有统一的名称。
GenericTypeHandler
1 | template <typename GenericType> |
StringTypeHandler
1 | // HACK: If a class is declared as DLL-exported in MSVC, it insists on |
对应类的关系图:
低配版release来节省资源
在proto文件中增加配置,产出不支持reflection/descriptor的MessageLite子类,而不是Message子类。
1 | option optimize_for = LITE_RUNTIME |