RapidJson的设计实现解读
- 2019 年 11 月 28 日
- 筆記
一、Rapidjson的DOM表示
DOM对象是不是似曾相熟,比如常听到浏览器解析http响应构建的DOM对象。DOM对象是个语言无关的,保存XML或者HTML文档的树状结构。
JSON其实是一个网络对象,它比XML、更简洁更方便在网络传输。DOM、和JSON、的关系是DOM、是JOSN串在内存中的表示。
1.1 类GenericDocument和类GenericValue
类Document描述了RapidJson的DOM结构。类Document是通用模板GenericDocument类UTF8的特化。
//! GenericDocument with UTF8 encoding typedef GenericDocument<UTF8<> > Document;
按照Json语法,这里的Document类型可以是Object,Array,Number,Stirng,Boolean和Null的任意一种类型。其他的都是非法的。
//! Type of JSON value enum Type { kNullType = 0, //!< null kFalseType = 1, //!< false kTrueType = 2, //!< true kObjectType = 3, //!< object kArrayType = 4, //!< array kStringType = 5, //!< string kNumberType = 6 //!< number };
所有的GenericValue都是基于以上合法type的json串做处理,代码中大量使用了 RAPIDJSON_NOEXCEPT做合法性验证。
GenericDocument类继承了GenericValue类。我们先来看看GenericValue都是怎么定义的。
1.2 GenericValue表示了DOM的一些基本元素和操作
GenericValue定义包括了DOM一些基本生成、替换、删除和查找(增删改查)成员函数。Value类则是用模板特化了更常用UTF-8编码的。
typedef GenericValue<UTF8<> > Value;
1.2.1 生成函数
GenericValue定义了以下构造函数:
- 无参数默认构造函数
- C11中Move语义的构造函数
- Copy语义的构造函数
- 从GenericDocument来得到的构造函数
- 各种基本数据类型(Int,String等等)来得到的构造函数
除此还定义了=操作符的函数和CopyFrom深拷贝函数
1.2.2 替换删除
- Set和Get方法。调用 `SetXXX()` 方法 – 这些方法会调用析构函数,并重建空的 Object 或 Array:
- []操作符
- 迭代器
- 成员追加或者插入节点。在插入节点的过程中需要注意 `document` 和 `value` 的生命周期并且正确地使用 allocator 进行内存分配和管理。
插入节点的一些样例:
这里有个person类,需要追加一个address属性。一个简单有效的方法就是修改上述 `address` 变量的定义,让其使用 `person` 的 allocator 初始化,然后将其添加到根节点。
Documnet address(&person.GetAllocator()); person["person"].AddMember("address", address["address"], person.GetAllocator());
不想通过显式地写出 `address` 的 key 来得到其值,可以使用迭代器来实现:
auto addressRoot = address.MemberBegin(); person["person"].AddMember(addressRoot->name, addressRoot->value, person.GetAllocator());
此外,还可以通过深拷贝 address document 来实现:
Value addressValue = Value(address["address"], person.GetAllocator()); person["person"].AddMember("address", addressValue, person.GetAllocator());
1.2.3 查找
- FindMemeber和HasMember方法。
- 比较操作符==和!=
- 类型判断符isXXXType()
1.3 GenericDocument提供了更直观的API
GenericDocument是继承了GenericValue的封装,提供一套更容易操作的api。Parse函数用于解析,并且提供了一些配套函数以及获取解析结果,解析出错码。
GenericDocument的几个关键成员包括:
- Encoding:解析和存储编码格式
- StackAllocator:栈的内存分配器,为什么需要这个呢,可以带来效能提升吗
- Allocator:内存分配器,可以用自带的,或者自己实现内存分配器,
1.3.1 内存分配器
`GenericDocument` 的缺省分配器是 `MemoryPoolAllocator`。此分配器实际上会顺序地分配内存,并且不能逐一释放。当要解析一个 JSON 并生成 DOM,这种分配器是非常合适的。
RapidJSON 还提供另一个分配器 `CrtAllocator`,当中 CRT 是 C 运行库(C RunTime library)的缩写。此分配器简单地读用标准的 `malloc()`/`realloc()`/`free()`。当我们需要许多增减操作,这种分配器会更为适合。然而这种分配器远远比 `MemoryPoolAllocator` 低效。
从外部传入一个定义好一个大数组也可以算是内存分配器。一个样例如下:
char valueBuffer[4096]; char parseBuffer[1024]; MemoryPoolAllocator<> valueAllocator(valueBuffer, sizeof(valueBuffer)); MemoryPoolAllocator<> parseAllocator(parseBuffer, sizeof(parseBuffer)); DocumentType d(&valueAllocator, sizeof(parseBuffer), &parseAllocator); d.Parse(json);
若解析时分配总量少于 4096+1024 字节时,这段代码不会造成任何堆内存分配(经 `new` 或 `malloc()`)。
使用者可以通过 `MemoryPoolAllocator::Size()` 查询当前已分的内存大小。那么使用者可以拟定使用者缓冲区的合适大小。
另外需要说明的是,`Allocator` 定义当 `Document`/`Value` 分配或释放内存时使用那个分配类。`Document` 拥有或引用到一个 `Allocator` 实例。而为了节省内存,`Value` 并没有Allocator。如果需要Allocator,需要从Document获取。
许多 DOM 操作 API 中要提供分配器作为参数。由于这些 API 是 `Value` 的成员函数,不希望为每个 `Value` 储存一个分配器指针。
1.4.1 触发解析
- Parse()函数
- 用于流的ParseStream函数
- 原位解析
什么是原位解析?
原位解析把分配开销及内存复制减至最小。
原位解析最适合用于短期的、用完即弃的 JSON。实际应用中,这些场合是非常普遍的,例如反序列化 JSON 至 C++ 对象、处理以 JSON 表示的 web 请求等。
使用原位解析的前置限制条件
- 整个 JSON 须存储在内存之中。
- 流的来源缓码与文档的目标编码必须相同。
- 需要保留缓冲区,直至文档不再被使用。
- 若 DOM 需要在解析后被长期使用,而 DOM 内只有很少 JSON string,保留缓冲区可能造成内存浪费。
1.4.2跟踪解析过程
解析过程顺利完成,`Document` 便会含有解析结果。当过程出现错误,原来的 DOM 会维持不变。可使用 `bool HasParseError()`、`ParseErrorCode GetParseError()` 及 `size_t GetErrorOffset()` 获取解析的错误状态。
获取错误的原因,以及错误开始的位置的一个样例如下:
Document d; if (d.Parse(json).HasParseError()) { fprintf(stderr, "nError(offset %u): %sn", (unsigned)d.GetErrorOffset(), GetParseError_En(d.GetParseErrorCode())); // ... }
1.4.3 Swap语义的 函数
类似于std::swap语义的接口, GenericDocument::swap(one, other)
1.4.4注意事项
源代码有提醒注意的是,GenericDocument没有实现任何虚接口,也包括没有实现析构函数,所以避免使用delete GenericDocument这种写法。
Rapidjson大量使用了浅拷贝,如果采用了浅拷贝,注意局部对象的使用 不超过对象生存范围,防止使用了被析构的对象。
二、RapidJson的SAX操作
SAX(Simple API for XML)是对XML的简单操作API的集合。其实这里使用了SAX概念集来描述操作JSON(或者内存中DOM,Document)的操作。
这个SAX还包含了以下的特性:
- 基于事件驱动模型,读取XML元素时触发回调方法
- 状态独立处理,元素处理不依赖于其他元素
- 串行化处理,只能逐个元素处理,没有回头路,不能回到文档的更早部分
2.1有哪些SAX事件
2.2 GoF设计模式解耦SAX和DOM
Accept(Handler &) const:bool 使用了Gof访问者设计模式,在不改变对象类的前提下,定义新操作。一个样例如下:
Writer<StringBuffer> writer(buffer); d.Accept(writer);
2.2.1 AOF的使用场合
常用的场景有输出字符串的字符,或者深拷贝object类型
2.2.2 这样设计的好处
实际上,`Value::Accept()` 是负责发布该值相关的 SAX 事件至处理器的。通过这个设计,`Value` 及 `Writer` 解除了偶合。`Value` 可生成 SAX 事件,而 `Writer` 则可以处理这些事件。