TN002: 持久化对象数据格式
By zhkza99c
这篇开始发现翻译起来开始吃力了,但是慢慢来,一点一点提高。。。希望有人给我挑错。
先在这里谢谢了。
这篇笔记描述了MFC关于支持C++对象持久化和当它(C++对象)被保存在文件里时对象数据的格式。使用 DECLARE_SERIAL 和 IMPLEMENT_SERIAL 这两个宏可以做到这一点。
The Problem 问题
MFC对持久化数据的实现依赖于一种紧凑的二进制格式,这种格式可以使许多对象紧凑的连接在一起儿构成一个文件。这种二进制格式提供了一个描述数据如何存储的结构,但是其实是对象的函数 序列化(Serialize) 提供存储对象的方法。
MFC通过使用类 CArchive .来解决结构化问题。一个 CArchive 对象持久化的联系,这种上下的联系从archive被建立时开始,一直到 CArchive::Close 这个函数被调用为止,无论是程序员显示的或者隐式的调用 CArchive 的析构方法之前, CArchive 都是存在的。
这个笔记描述了 CArchive 的成员函数ReadObject 和WriteObject 是如何实现的。 ReadObject 和 WriteObject 不是直接调用的,而是通过DECLARE_SERIAL 和IMPLEMENT_SERIAL 宏使用特定类型的安全插入和提取操作自动生成的。
class CMyObject : public CObject
{
DECLARE_SERIAL(CMyObject)
};
IMPLEMENT_SERIAL(CMyObj, CObject, 1)
// 用法示例(ar 是一个CArchive&)
CMyObject* pObj;
CArchive& ar;
ar << pObj; // 调用 ar.WriteObject(pObj)
ar >> pObj; // 调用 ar.ReadObject(RUNTIME_CLASS(CObj))
这个笔记描述的代码都可以在MFC的源代码ARCOBJ.CPP中找到。这也意味着 CArchive 的实现可以在ARCCORE.CPP里找到。
存储数据到存储空间(CArchive::WriteObject)
成员函数 CArchive::WriteObject 构建出数据头用来重建对象,这个数据有两部分组成:对象的类型和对象被写出时的标识,因此无论有多少个指向该对象的指针,该对象仅仅有一个拷贝被存储。(包括循环指针).
存储(插入)和重建(提取)对象以来于数个“常量的列表”,下面是以二进制保存的值和所提供的重要的archive数据信息(注意‘w’打头意味着这些都是16位的数据(word型)):
Tag
|
Description
|
wNullTag
|
NULL对象指针时使用(0)
|
wNewClassTag
|
表明类的描述是一个新的archive 上下关联。(-1).
|
wOldClassTag
|
表明正在被读取的类的对象可以从上下关联找到。(0x8000).
|
当存储对象时,archive包含了一个 CMapPtrToPtr ( m_pStoreMap ) ,它是一个从被存储对象到一个32位持久化标示符(PID)的映射。PID是分配给每一个对象和每一个类名称的独一无二的标示,它被存在archive的上下关联中。这些PID的值是从1开始的。需要注意的是这些PID在archive作用范围之外是没有任何意义的,并且特别是不要把他们同记录号码或其他标示项所搞混。
自从MFC4.0版本开始, CArchive 类已经被扩张为支持非常大的archive了。在之前的版本中,PID是16位的,限制archive的对象到0x7FFE (32766)这么大,现在PID是32位的了,但是他们除非超过了0x7FFE都是以16位写出的。大的PID是记录在 0x7FFF之后的32位的PID。这一个技术保留了向前的兼容性。
当存储一个对象到archive的要求产生时(通常是通过全局性插入操作),将会检测指向 CObject 的指针是否是NULL,如果这个指针是NULL的,那么wNullTag 这个标志位将被插入到archive流中。
如果我们有一个有效的能被序列化的对象指针(这个类是一个 DECLARE_SERIAL 的类),我们检测 m_pStoreMap 来看看这个对象是不是已经被存储过了,如果适当,那么插入一个与这个对象相联系的32位的PID。
如果这个对象之前没有被存储,那么有两个可能性我们必须考虑到:对于archive的上下关联来说无论是对象本身还是他明确的类型(就是说,类)都是新的,或者说这个对象是一个已经被看到的明确的类型。来确定是否这个类型已经被看到我们需要从 m_pStoreMap 查询一个 CRuntimeClass 对象,这个对象是否是我们已经存储的一个 CRuntimeClass对象。 如果我们之前看到了这个类, WriteObject 就会插入一个位标签或上wOldClassTag再与上这个索引( WriteObject inserts a tag that is the bit-wise OR'ing of wOldClassTag and this index.)。如果这个 CRuntimeClass 对于archive是新的上下关联,那么 WriteObject 给这个类分配一个新的PID并且插入到archive中,并且在wNewClassTag 值之前。
这个类的说明会在之后使用 CRuntimeClass 成员函数 Store 插入到archive中。 CRuntimeClass::Store 插入这个类架构的号码(之前有提到)和这个类的ASCII文本名称。注意是使用ASCII文本而并不保证在archive中是独一无二的,因此一个明智的行为是标示你的数据文件来防止冲突。插入类的信息后,archive将对象置入 m_pStoreMap 并且调用Serialize成员函数来向archive插入这个类的特殊数据。在调用 Serialize 之前将对象置入 m_pStoreMap可以阻止重复拷贝这一个对象。
当返回到初始化调用时(通常是在对象网络的底部), Close 这个archive是很重要的。如果其他的 CFile 操作被实施了,必须调用 CArchive 的成员函数 Flush 。没有做这个会导致一个坏的archive的产生。
注意 每一个archive的索引数目是被强制限制在0x3FFFFFFE 之内的。这个数目象征这一个archive中最大可以保存的相互不同的对象和类的数目。
从存储空间读取数据(CArchive::ReadObject)
读取对象使用 CArchive::ReadObject 的成员函数,它是 WriteObject .的逆过程, ReadObject 不是被每个用户代码直接调用的。用户代码需要调用类型安全的读取操作,这个操作调用所期待的 CRuntimeClass .的 ReadObject 。这确定了读取操作的完整性。
WriteObject 的行为分配可以增长的PID,这个PID是从1开始的(0是被NULL对象所预先定义的),ReadObject 的行为可以使用数组来包含archive上下关联的状态。当一个PID从存储控件读取时,如果这个PID超过了当前 m_pLoadArrayd 上限,那么ReadObject 就会知道这是一个新的对象(或者新的类的描述)。
Schema号码
当碰到 IMPLEMENT_SERIAL 时Schema号码被分配给类,表示这个类的实现的版本号码。Schema涉及到类的实现,而不是所给持久化类出现的次数(通常同对象的版本关联)。
如果你尝试着包含同一个类的数个不同的实现,如同你修正你的对象的Serialize 成员函数的实现一样增长你的schema会让你写下的代码可以读取之前存储的旧的版本的实现。
CArchive::ReadObject 成员函数当遇到和内存中存储的schema号码与持久化存储对象的schema号码不一致时会抛出一个 CArchiveException。 想要修复这个异常并不简单。
你可以使用 VERSIONABLE_SCHEMA 与你的schema版本来保证这个异常不被抛出,你的代码能采取适当的行为在它的 Serialize 函数中,这种行为是通过检查 CArchive::GetObjectSchema .的返回值来实现的。
直接调用序列化(Serialize)
当通常的对象的辅助操作WriteObject 和ReadObject 并不是被需要时,archive scheme有很多情况。通常的情况是将数据序列化放入 CDocument . 这种情况下 CDocument 的 Serialize 成员函数被直接调用了,但并不是通过插入或者取出操作。该文档的内容可能反过来使用更普遍的对象arvhive scheme。
直接调用 Serialize 有以下的优点和缺点:
没有额外的字节在archive之前或者在对象被序列化过程中加入,这不仅仅将存储的数据变小了,而且允许你通过执行 Serialize 的规则来操作更多的文件格式。
MFC是协调的,因此 WriteObject和ReadObject 实现和相关的收集不会被你的应用程序连接。除非你因为一些且他的目的需要更普遍的对象archive scheme。
你的代码不需要重写旧的schema号码。这使你的文档序列化代码有义务的为schema numbers编码,文件格式版本号码或者任何梦幻般的号码都要在你的数据文件的开始得到描述。
任何通过直接调用Serialize序列化的对象不能使用CArchive::GetObjectSchema或者必须控制一个返回值(NULL)-1,指出未知的版本。
因为 Serialize 在你的文档里被直接调用,通常不可能为这个文档的子对象文档化来参照他们的母文档。这些对象必须显式的给一个指向他们文档容器,或者你必须在他们之后的指针被文档化前使用 CArchive::MapObject 函数来应声这个CDocument 指针到一个PID
就像笔记前述的,当直接调用串行化时你应该自己为版本号和类的信息编码,允许你之后改变格式,却仍然保持同旧文件的兼容性。CArchive::SerializeClassRef函数在直接串行化一个对象或者调用一个基类之前应该被显式的调用。