首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >RapidJson的设计实现解读

RapidJson的设计实现解读

原创
作者头像
mariolu
修改2019-11-28 10:19:13
2.7K0
修改2019-11-28 10:19:13
举报

一、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): %s\n", 
        (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` 则可以处理这些事件。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、Rapidjson的DOM表示
    • 1.1 类GenericDocument和类GenericValue
      • 1.2 GenericValue表示了DOM的一些基本元素和操作
        • 1.2.1 生成函数
        • 1.2.2 替换删除
        • 1.2.3 查找
      • 1.3 GenericDocument提供了更直观的API
        • 1.3.1 内存分配器
        • 1.4.1 触发解析
        • 1.4.2跟踪解析过程
        • 1.4.3 Swap语义的 函数
        • 1.4.4注意事项
    • 二、RapidJson的SAX操作
      • 2.1有哪些SAX事件
        • 2.2 GoF设计模式解耦SAX和DOM
          • 2.2.1 AOF的使用场合
          • 2.2.2 这样设计的好处
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档