Protobuf-Repeated相关类

Repeated类型

field包含2种类型:

  • Strings/Message类型使用RepeatedPtrFields
  • enum / primitive类型使用RepeatedFields

核心类关系图如下:

avatar

类RepeatedField

模板类,为primitive类型数据提供repeated类型容器。内部实现为连续内存的array(保存的内容就是primitive对象),并且对外提供iterator方式来访问。

array起始大小为4(static const int kInitialSize = 4;),当长度不够的时候会分配新的内存(max(total_size_ * 2, new_size))以及内存拷贝,所以如果repeated的成员很多,使用Reserve()接口能节省array增大时的内存分配和数据拷贝成本。

类RepeatedPtrFieldBase

RepeatedPtrFields的父类(不是模板类,提供了多个模板函数),本身保存/管理的数据类型为void*(message对象的实际地址,也是通过连续内存array来保存)。

RepeatedPtrFieldBase类并不感知自己管理的具体是什么message,通过模板函数的模板参数TypeHandler来为各种数据类型数据提供服务,例如:

1
2
3
4
5
6
7
	template <typename TypeHandler>
void RepeatedPtrFieldBase::Clear() {
for (int i = 0; i < current_size_; i++) {
TypeHandler::Clear(cast<TypeHandler>(elements_[i]));
}
current_size_ = 0;
}

因为array中保存的是同一个descriptor对应的message,只是各个message中所包含的数据不一样,为了节省下message对象分配/删除的成本,所以message可以被clear(clear操作会将primitive类型的field设置为0,其余类型field调用自身的clear()接口处理,例如string类型的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;

对应内存分布如下:
avatar

类RepeatedPtrField

模板类,RepeatedPtrFieldBase的子类,为Strings/Message类型数据提供repeated类型容器。

Q: 这里RepeatedPtrField类是RepeatedPtrFieldBase类唯一的子类,是否也没有必要这样区分父类/子类呢?
Answer如下:

提前铺垫(父类/子类的分工):
  • RepeatedPtrFieldBase(非模板类,提供模板函数)负责的是最基本的基于array<void*>的操作,并不感知所保存的内容的数据类型,所有需要区分类型的操作都有模板类型TypeHandler来负责;
  • RepeatedPtrField(模板类)感知数据类型(数据类型由模板参数Element提供),并且对外的接口都是基于类型Element的。针对Element的操作则由TypeHandler来负责,并且通过父类RepeatedPtrFieldBase模板函数的参数传递给父类。

这种分工可以在RepeatedPtrField的很多函数上体现,例如:

1
2
3
4
template <typename Element>
inline void RepeatedPtrField<Element>::RemoveLast() {
RepeatedPtrFieldBase::RemoveLast<TypeHandler>();
}
本质原因:

某些情况下无法感知子类(模板类)RepeatedPtrField的模板参数Element,所以并不清楚具体子类,只能指向父类RepeatedPtrFieldBase。

例如在GeneratedMessageReflection::AddMessage()中,其实message子类中保存的是RepeatedPtrField对象(可以参考student.proto中的例子),所以只能将使用父类RepeatedPtrFieldBase指针指向RepeatedPtrField的对象,然后:

  1. 调用RepeatedPtrFieldBase::AddFromCleared(),尝试获取已cleared但未释放的message对象。如果没有,就继续;
  2. 获取一个prototype(指向真实Message子类对象的父类Message指针):
    (2.1)先看RepeatedPtrFieldBase的array<RepeatedPtrField >是否有成员,如果有就使用;
    (2.2)调用factory->GetPrototype()创建一个;
  3. 调用prototype(指向真实Message子类对象的父类Message指针)的Message::New()接口,创建出一个真实field_descriptor对应的Message子类对象;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Message* GeneratedMessageReflection::AddMessage(
Message* message, const FieldDescriptor* field,
MessageFactory* factory) const {

// 省略非核心代码

// We can't use AddField<Message>() because RepeatedPtrFieldBase doesn't
// know how to allocate one.
RepeatedPtrFieldBase* repeated =
MutableRaw<RepeatedPtrFieldBase>(message, field);
Message* result = repeated->AddFromCleared<GenericTypeHandler<Message> >();
if (result == NULL) {
// We must allocate a new object.
const Message* prototype;
if (repeated->size() == 0) {
prototype = factory->GetPrototype(field->message_type());
} else {
prototype = &repeated->Get<GenericTypeHandler<Message> >(0);
}
result = prototype->New();
repeated->AddAllocated<GenericTypeHandler<Message> >(result);
}
return result;

}
Q:这里的TypeHandler是在哪里定义的呢?类RepeatedPtrField中并没有提供接口来针对不同数据类型设置typehandler?  

Answer如下:

定义在repeated_field.h中,根据模板类RepeatedPtrField<>模板参数的不同(Element或者string),继承了不同的父类(因为这里子类自己并没有独有的数据/行为,所以用这种方式来选择使用哪种handler):

1
2
3
4
5
6
7
template <typename Element>
class RepeatedPtrField<Element>::TypeHandler
: public internal::GenericTypeHandler<Element> {};

template <>
class RepeatedPtrField<string>::TypeHandler
: public internal::StringTypeHandler {};

typehandler是直接子类RepeatedPtrField在调用父类RepeatedPtrFieldBase的模板函数时,通过模板参数直接传入父类RepeatedPtrFieldBase,可以通过GeneratedMessageReflection中使用的例子来看:

1
2
3
4
5
6
7
8
const Message& GeneratedMessageReflection::GetRepeatedMessage(
const Message& message, const FieldDescriptor* field, int index) const {

…… //省略

return GetRaw<RepeatedPtrFieldBase>(message, field)
.Get<GenericTypeHandler<Message> >(index);
}

模板函数RepeatedPtrFieldBase::Get(),这里的TypeHandler就是GenericTypeHandler

1
2
3
4
5
6
template <typename TypeHandler>
inline const typename TypeHandler::Type&
RepeatedPtrFieldBase::Get(int index) const {
GOOGLE_DCHECK_LT(index, size());
return *cast<TypeHandler>(elements_[index]);
}

注意:这里TypeHandler是RepeatedPtrField类的protected成员,为了不让用户再将RepeatedPtrField作为父类来使用:

1
2
3
4
5
6
protected:
// Note: RepeatedPtrField SHOULD NOT be subclassed by users. We only
// subclass it in one place as a hack for compatibility with proto1. The
// subclass needs to know about TypeHandler in order to call protected
// methods on RepeatedPtrFieldBase.
class TypeHandler;

类GenericTypeHandler

针对message的typehandler

类StringTypeHandler

StringTypeHandlerBase的子类,在父类基础上增加了SpaceUsed()接口。

1
2
3
4
5
6
	class LIBPROTOBUF_EXPORT StringTypeHandler : public StringTypeHandlerBase {
public:
static int SpaceUsed(const string& value) {
return sizeof(value) + StringSpaceUsedExcludingSelf(value);
}
};

这里需要理解string的数据结构来理解这段代码了,从代码看start/end是保存在string对象的前第一/前第二个位置void*(sizeof(void*)为8个byte)的。

1
2
3
4
5
6
7
8
9
10
11
int StringSpaceUsedExcludingSelf(const string& str) {
const void* start = &str;
const void* end = &str + 1;

if (start <= str.data() && str.data() <= end) {
// The string's data is stored inside the string object itself.
return 0;
} else {
return str.capacity();
}
}

Q: 为什么需要区分父类/子类呢?直接使用StringTypeHandler即可啊

Answer如下:

1
2
3
4
5
6
7
8
// HACK:  If a class is declared as DLL-exported in MSVC, it insists on
// generating copies of all its methods -- even inline ones -- to include
// in the DLL. But SpaceUsed() calls StringSpaceUsedExcludingSelf() which
// isn't in the lite library, therefore the lite library cannot link if
// StringTypeHandler is exported. So, we factor out StringTypeHandlerBase,
// export that, then make StringTypeHandler be a subclass which is NOT
// exported.
// TODO(kenton): There has to be a better way.