oman 发表于 2011-6-8 14:36:25

第二代文件格式的序列化存储流程分析

花了两天时间仔细研究了一下array贡献的第二代文件序列化存储,收益颇多。今后将得益于它的很强的可扩展性。通过自身的体验,感觉应该还有很多初学者可能需要花费几个小时才能看透,所以特将自己分析的流程发上来,以加快想搞明白其流程的初学者的学习速度。这两天身体状况不太好,两眼通红,眼睛发涩,所以在写的过程中难免有些拼写错误什么的,流程的分析也难免有纰漏或不妥,还请高手指正,尤其是array本人。

我们以将节点写入文件为例,节点读取与此相同,看看究竟是如何将节点写入文件的,我们从文件读写插件的最顶层入口函数来看,即
virtual WriteResult writeNode( const osg::Node& node, const std::string& fileName, const Options* options ) const
因为osg第二代文件格式读写插件同时支持文本、二进制以及XML格式的文件,所以,在写文件前要确定具体要写入什么文件,读写不同的文件格式需要用到的序列化器不同,文件打开方式也不同,
进入writeNode后会调用Options* prepareWriting( WriteResult& result, const std::string& fileName, std::ios::openmode& mode, const Options* options ) const
该函数就完成上面的工作,该函数根据文件扩展名判断如果是osgt格式,就向options中添加文本格式的参数选项,如果是osgx格式,就添加XML格式选项,以让后续的写入操作根据该选项进行相应文件格式的读写,否则以二进制格式读写;
做好以上准备工作后,就像以前一样将文件数据读入文件流开始向文件流写入数据,即进入virtual WriteResult writeNode( const osg::Node& node, std::ostream& fout, const Options* options ) const
针对文本、XML、二进制格式的文件分别对应三种不同的输出指示器,在OutputStream类中进行数据写入时要调用相应的输出指示器,所以,首先要获取相应类型的输出指示器;该工作通过函数OutputIterator* writeOutputIterator( std::ostream& fout, const Options* options )来完成,该函数从options中查找是否有文本或者XML的文件格式选项(从刚才的分析我们已经知道,如果是osgt或osgx格式的话,在prepareWriting中已经添加进来),如果有对应的选项,就创建相应的输出指示器,如果两种都没有,就创建二进制格式的输出指示器,除文本格式之外,对于XML格式和二进制格式,该函数除了创建相应的输出指示器外,同时还像文件流写入了文件头信息,对于XML格式,直接写入XML文件头,对于二进制格式,写入MD5码;从上面的分析我们不难看出,对于文本和XML格式的文件,我们简单的添加supportsExtension来让插件支持我们自己的扩展名文件类型是做不到的,插件会把它当做二进制来对待,无法正确处理,所以,扩展名得扩展只适用于二进制格式;
得到输出指示器后构造一个OutputStream对象开始写数据。
首先是调用void OutputStream::start( OutputIterator* outIterator, OutputStream::WriteType type ),将输出指示器传给OutputStream并向文件流写入文件的标志信息,如文件内容类型标示(如场景、对象、图像)、版本号等信息,接下来调用void OutputStream::writeObject( const osg::Object* obj )从节点对象读取节点数据。下面进入该函数,首先获取一个对象唯一ID,然后向流中写入对象类名(如osg::Group),然后写入开始大括号’{’,接下来写入对象的数据,写入对象数据后写入结束大括号’}’;上面的写入操作都是通过刚才传入的输出指示器来完成;
        下面我们将如何写入对象数据来分解开进行分析,这是写入操作的核心所在。该过程在void OutputStream::writeObjectFields( const osg::Object* obj )中完成,我们现在就来看看writeObjectFields都玩了哪些花样;一定要看仔细,正是它玩的花样,才使得我们可以在不修改该文件操作插件的情况下通过扩展的方式让自己的节点对象也可以读写的,所以一定要看仔细了;
首先要根据类名获取该类的wrapper(所有要能够将数据写入文件的类都要对应有一个该类的包装类wrapper,所有的wrapper统一由wrapper管理器来管理,每添加一个wrapper类型,自己要将自己注册到该管理器,否则无法对该类型的对象进行读写操作),如果要让自己的对象能够进行文件读写,需要扩展自己的类的wrapper类,在wrapper类中定义要读写哪些数据字段;
类是有继承关系的,那么在进行读写操作时,还是需要各个类自己操作自己本身的成员,一个子类只负责自己扩展出来的成员字段的读写操作,父类的那些成员又父类的wrapper去处理,各司其职。而系统本身是没有办法知道一个子类上面都有哪些父类、祖父类等关联类信息的,所以需要我们告诉它,该工作通过在构造wrapper时指定,将该子类关联的所有上层类名称传进去,即associates参数,在每个wrapper类中有一个associates的数组。
继续上面的分析过程,在writeObjectFields中获取到要写入文件的节点对象(根据类名获得,所以,自己派生的对象要能像osg对象那样能够用这种方式进行读写操作,必须从osg::Object派生,且必须实现ClassName接口)对应的wrapper后,遍历该wrapper的关联类数组,然后再根据关联类获取对应的wrapper进行数据写入工作。在继续该写入过程详细分析之前,需要补充说明一下,注册wrapper时向该wrapper传入的管理类信息包含了该类本身,所以,这里的写入操作统一在遍历关联类数组过程中进行,所以,在注册wrapper时,一定要在最后将类自身也添加到关联类信息里,否则只能读写上层类的数据,本类本身的信息会丢失。
接下来的分析就相对要集中了,就是针对具体的类类型进行数据的写入操作了。如果需要向文件中写入每个类的字段刚要信息,就将每个类的属性字段名称及类型以刚要的形式写入,然后再写该类的数据信息,默认情况下不写入;那么我们接着看写对象的数据字段的内容。该过程由bool ObjectWrapper::write( OutputStream& os, const osg::Object& obj )完成,下面我们来进到里面探个究竟。
在分析之前我们需要插入一段前奏,然后才能继续下面的过程。通过上面的分析,我们知道怎么让插件能够读写每种类对象了(通过扩展wrapper并注册),也知道了怎么区别对待不同类型的文件(Txt、XML、Binary,通过不同的指示器),还有一个重要的问题没有提到,那就是如何知道要读写一个类的哪些数据成员,以及如何调用数据获取接口。在注册wrapper时,除了给wrapper设置其对应的类名、类对象原型、关联类描述之外,还要设置通过何种序列化器对哪些字段进行读写操作。我们通过代码不难看到ADD_USER_SERIALIZER、ADD_OBJECT_SERIALIZER、ADD_DOUBLE_SERIALIZER等等身影,通过这些宏就设定好了要读写该wrapper对应的类的数据字段。一个对象有多少属性字段,就会对应多少序列化器,每个序列化器负责该属性字段的具体的读写操作,wrapper对象会将所有加入的序列化器保存在一个数组中。此外,对于不同的字段数据类型,对应有相应类型的序列化器,如int、double、osg::Vec3d以及对象类型等等,操作相应类型的属性字段要选择正确的序列化器。
通过上面的分析,我们应该知道了如何设定类的哪些属性数据可进行文件存储,接下来我们来看看是如何调用相关的属性访问接口来进行数据存取的。方法用的就是函数指针,在添加序列化器时,相应的序列化器会根据传入的属性字段名称自动创建对应的属性访问函数指针(getter、setter)或字段读写函数指针(reader、writer)并将其传递给序列化器的构造函数(函数指针作为函数参数传递,这些函数都不会有重载函数,基本都是getXXX、setXXX,大可放心使用),然后序列化器会记录下来这些函数指针,在序列化器进行数据字段的读写时调用该函数指针来进行数据字段的读写操作;
好了,前奏到此为止,不要忘了我们还没有进入bool ObjectWrapper::write( OutputStream& os, const osg::Object& obj )呢。现在我们来看这个函数就很简单了,它也就是遍历该wrapper对象的所有序列化器,调用序列化器的write函数将每个属性字段写进去。对于UserSerializer是传递属性字段读写函数指针,对于其他类型的序列化器,传递的是属性字段的访问函数指针,两者最终都是通过OutputStream的”<<”重载操作符将字段值写入到流中,进一步我们可以看到OutputStream的”<<”重载操作符函数最终通过调用最开始时传入OutputStream的输出指示器的类型写入函数来完成最终的写入操作;针对不同类型的格式控制(txt、XML、Binary)都是在相应的输出指示器中完成。
现在我们知道,通过该插件,我们要让该插件支持我们自己扩展的对象类型,只要创建相应的wrapper并注册进来就OK,非常方便。另外一方面,可能在开发自己的应用系统时,需要定义自己的文件格式,这时,如果是二进制的格式的话,直接通过supportsExtension添加自己的扩展名支持即可,在应用系统中通过addFileExtensionAlias来指定一下。此外还有一个重要的扩展可能对我们开发自己的系统更为重要,那就是文件数据的加密存储,简单起见我们可以重写插件的writeOutputIterator和readOutputIterator,在文件头中加入自己的加密数据,如base64/md5加密数据,并做相应的检测处理,也非常方便;

oman 发表于 2011-6-10 08:26:12

更正一下,上面说的文本格式、XML格式也可以扩展自己的格式,不过在读写时要调用带有Optioins参数的接口,将文本或XML标识设置一下;

array 发表于 2011-6-10 08:32:27

非常感谢您的共享,呵呵,也谢谢帮助我推广第二代格式。有个小地方值得修正一下:最后一段您提出通过重写OutputIterator来实现自己的数据加密,其实我已经提供了更简单的方式,也就是osgDB:: BaseCompressor,通过派生新的compressor我们就可以实现对文件内容的压缩和解压缩,或者加密和解密,或者加入自己的头信息等等

我针对serializers写的文档可以参看:
http://www.openscenegraph.org/projects/osg/wiki/Support/KnowledgeBase/SerializationSupport

oman 发表于 2011-6-10 08:34:31

噢,非常感谢提醒,受教了。

tianxiao888 发表于 2011-6-10 10:38:41

感谢楼主的分享,我也一直没时间理出序列换存储的头绪~~

gis_wudi 发表于 2011-6-10 14:40:20

第二代文件格式是在哪个版本推出的,谢谢楼主分享,辛苦!
页: [1]
查看完整版本: 第二代文件格式的序列化存储流程分析