正文: “文档序列化”显然可以分成两个部分“写文件”和“读文件”。我在本文中也将从这两个方面来为你挖掘文档序列化的奥秘。 /////////////////////////////////////////////// /* 1.“写读文件”的共同基础 */ ////////////////////////////////////////////// 无论是写还是读都等借助CRuntimeClass结构以及一些神秘的宏的帮助。在前几篇文章中我们没少和CRuntimeClass结构打交道,什么MFC执行期类型识别,什么动态创建技术等等。提到的这两种技术是文档序列化的基础,下面我们就看看为什么可以这么说: 除了与动态创建有关的成员外,在CRuntimeClass结构中还与序列化有关的重要成员有: //in afx.h struct CRuntimeClass { // Attributes ...// LPCSTR m_lpszClassName; int m_nObjectSize; UINT m_wSchema; // schema number of the loaded class ...// void Store(CArchive& ar) const; static CRuntimeClass* PASCAL Load(CArchive& ar, UINT* pwSchemaNum); const AFX_CLASSINIT* m_pClassInit; ...// } 其中两个重要函数Store和Load的源代码如下:这两个函数主要写和读a runtime class description,其中包括m_lpszClassName和m_wSchema(版本号); //in arccore.cpp void CRuntimeClass::Store(CArchive& ar) const // stores a runtime class description { WORD nLen = (WORD)lstrlenA(m_lpszClassName); ar << (WORD)m_wSchema << nLen; ar.Write(m_lpszClassName, nLen*sizeof(char)); } // loads a runtime class description CRuntimeClass* PASCAL CRuntimeClass::Load(CArchive& ar, UINT* pwSchemaNum) { WORD nLen; char szClassName[64];
WORD wTemp; ar >> wTemp; *pwSchemaNum = wTemp; ar >> nLen;
// load the class name if (nLen >= _countof(szClassName) || ar.Read(szClassName, nLen*sizeof(char)) != nLen*sizeof(char)) { return NULL; } szClassName[nLen] = '/0';
// match the string against an actual CRuntimeClass CRuntimeClass* pClass = FromName(szClassName); if (pClass == NULL) { // not found, trace a warning for diagnostic purposes TRACE(traceAppMsg, 0, "Warning: Cannot load %hs from archive. Class not defined./n",szClassName); }
return pClass; } 下面看看那两个神秘的宏DECLARE_SERIAL和IMPLEMENT_SERIAL吧! //in afx.h #define DECLARE_SERIAL(class_name) / _DECLARE_DYNCREATE(class_name) / AFX_API friend CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb); #define IMPLEMENT_SERIAL(class_name, base_class_name, wSchema) / CObject* PASCAL class_name::CreateObject() / { return new class_name; } / AFX_COMDAT AFX_CLASSINIT _init_##class_name(RUNTIME_CLASS(class_name)); / _IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, wSchema, / class_name::CreateObject, &_init_##class_name) / CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb) / { pOb = (class_name*) ar.ReadObject(RUNTIME_CLASS(class_name)); / return ar; } / 宏的定义验证了上面的“基础”一说; 下面是IMPLEMENT_SERIAL进行初始化时的辅助结构AFX_CLASSINIT及函数AfxClassInit的定义: // generate static object constructor for class registration void AFXAPI AfxClassInit(CRuntimeClass* pNewClass); struct AFX_CLASSINIT { AFX_CLASSINIT(CRuntimeClass* pNewClass) { AfxClassInit(pNewClass); } }; //in objcore.cpp void AFXAPI AfxClassInit(CRuntimeClass* pNewClass) { AFX_MODULE_STATE* pModuleState = AfxGetModuleState(); AfxLockGlobals(CRIT_RUNTIMECLASSLIST); pModuleState->m_classList.AddHead(pNewClass); AfxUnlockGlobals(CRIT_RUNTIMECLASSLIST); } 有了前面文章的基础,在这里我就不详细将宏展开详解了,有了上面基础,我们就可以"真刀真枪"的读写文件了,比较起来“写文件”较容易,所以就让我们拿它先开刀吧!^_^
////////////////////////////////////// /* 2.“写文件” */ ////////////////////////////////////// 大家都应该了解“写文件”的“导火索”是什么吧?你说对了"save"or"save as",下面我们就沿着这个导火索一路下行看看到底发生了什么吧? 这里大家会发现一个小问题:那就是你在MFC应用程序向导为你做的SDI or MDI代码中找不到 "save"or"save as"功能项的处理函数,但它的功能却还能淋漓尽致的展现在你面前。这是为什么呢?原来这些函数是由CDocument类提供的。你的Doc类继承CDocument类的同时也将这些处理函数完全集成了下来。下面我们就来看看他们的卢山真面目吧! 当你按下"save"or"save as"功能键(包括菜单中的和工具栏中的)后,应用程序将调用 CMyDoc::OnFileSave() or CMyDoc::OnFileSaveAs()(以后将只提及一个),但由于CMyDoc类继承了其基类 CDocument的处理函数,所以实际调用的是CDocument::OnFileSave(); //in doccore.cpp void CDocument::OnFileSave() { DoFileSave(); } BOOL CDocument::DoFileSave() { DWORD dwAttrib = GetFileAttributes(m_strPathName); if (dwAttrib & FILE_ATTRIBUTE_READONLY) { // we do not have read-write access or the file does not (now) exist if (!DoSave(NULL)) { TRACE(traceAppMsg, 0, "Warning: File save with new name failed./n"); return FALSE; } } else { if (!DoSave(m_strPathName)) { TRACE(traceAppMsg, 0, "Warning: File save failed./n"); return FALSE; } } return TRUE; } BOOL CDocument::DoSave(LPCTSTR lpszPathName, BOOL bReplace) // Save the document data to a file // lpszPathName = path name where to save document file // if lpszPathName is NULL then the user will be prompted (SaveAs) // note: lpszPathName can be different than 'm_strPathName' // if 'bReplace' is TRUE will change file name if successful (SaveAs) // if 'bReplace' is FALSE will not change path name (SaveCopyAs) { CString newName = lpszPathName; if (newName.IsEmpty()) { ...// if (!AfxGetApp()->DoPromptFileName(newName, bReplace ? AFX_IDS_SAVEFILE : AFX_IDS_SAVEFILECOPY, OFN_HIDEREADONLY | OFN_PATHMUSTEXIST, FALSE, pTemplate))//*** return FALSE; // don't even attempt to save }
CWaitCursor wait; if (!OnSaveDocument(newName)) { if (lpszPathName == NULL) { // be sure to delete the file TRY { CFile::Remove(newName); } CATCH_ALL(e) { TRACE(traceAppMsg, 0, "Warning: failed to delete file after failed SaveAs./n"); DELETE_EXCEPTION(e); } END_CATCH_ALL } return FALSE; } // reset the title and change the document name if (bReplace) SetPathName(newName); return TRUE; // success } CDocument::DoSave函数继续调用OnSaveDocument(...)函数来完成写入任务。 BOOL CDocument::OnSaveDocument(LPCTSTR lpszPathName) { CFileException fe; CFile* pFile = NULL; pFile = GetFile(lpszPathName, CFile::modeCreate | CFile::modeReadWrite | CFile::shareExclusive, &fe); ...// CArchive saveArchive(pFile, CArchive::store | CArchive::bNoFlushOnDelete); saveArchive.m_pDocument = this; saveArchive.m_bForceFlat = FALSE; TRY { CWaitCursor wait; Serialize(saveArchive); // save me saveArchive.Close(); ReleaseFile(pFile, FALSE); } ...//
SetModifiedFlag(FALSE); // back to unmodified
return TRUE; // success } 该函数创建了一个与要保存的文件相关联的CArchive实例saveArchive,并调用了函数 CMyDoc::Serialize(CArchive&ar); void CMyDoc::Serialize(CArchive& ar) { if (ar.IsStoring()) { // TODO:在此添加存储代码/写入 } else { // TODO:在此添加加载代码/读出 } } 看看该函数,你有些傻眼了,空函数几乎什么也没有,这又对了,因为MFC不知道你的数据是什么样式的,所以它没有这个能力越俎代庖。如果你没有为该函数添加代码,则该函数也就到此为止了,但我们要把其奥秘挖掘出来就不能到此结束,我们也用类似侯捷老师的Scribble例子给CMyDoc类加一点代码,改为: class CStroke:public CObject//线条类 { ...// protected: UINT m_nPenWidth; public: CArray<CPoint,CPoint>m_pointArray; } class CMyDoc:public CDocument { ...// CTypedPtrList<CObList,CStroke*>m_strokList; } void CMyDoc::Serialize(CArchive& ar) { if (ar.IsStoring()) { // TODO:在此添加存储代码/写入 } else { // TODO:在此添加加载代码/读出 } m_strokList.Serialize(ar); } void CStroke::Serialize(CArchive& ar) { if (ar.IsStoring()) { // TODO:在此添加存储代码/写入 ar<<(WORD)m_nPenWidth; m_pointArray.Serialize(ar); } else { // TODO:在此添加加载代码/读出 WORD w; ar>>w; m_nPenWidth=w; m_pointArray.Serialize(ar); } 下面我们可以继续我们的挖掘了: CTypedPtrList::Serialize函数被调用,由于CTypedPtrList并为改写Serialize函数,所以实际调用的是其基类CObList的Serialize函数; //in list_o.cpp void CObList::Serialize(CArchive& ar) { ASSERT_VALID(this); CObject::Serialize(ar); if (ar.IsStoring()) { ar.WriteCount(m_nCount);//*** for (CNode* pNode = m_pNodeHead; pNode != NULL; pNode = pNode->pNext) { ASSERT(AfxIsValidAddress(pNode, sizeof(CNode))); ar << pNode->data;//*** } } else { ...// } } 其中void CArchive::WriteCount(DWORD_PTR dwCount)函数用于将CObList中的表元素个书写入。 CArchive重载了<<运算符,代码如下: //in afx.inl _AFX_INLINE CArchive& AFXAPI operator<<(CArchive& ar, const CObject* pOb) { ar.WriteObject(pOb); return ar; } //in arcobj.cpp void CArchive::WriteObject(const CObject* pOb) { ...// // make sure m_pStoreMap is initialized MapObject(NULL); if (pOb == NULL) { // save out null tag to represent NULL pointer *this << wNullTag; } else if ((nObIndex = (DWORD)(DWORD_PTR)(*m_pStoreMap)[(void*)pOb]) != 0) // assumes initialized to 0 map { // save out index of already stored object if (nObIndex < wBigObjectTag) *this << (WORD)nObIndex; else { *this << wBigObjectTag; *this << nObIndex; } } else { // write class of object first CRuntimeClass* pClassRef = pOb->GetRuntimeClass(); WriteClass(pClassRef);//***
// enter in stored object table, checking for overflow CheckCount(); (*m_pStoreMap)[(void*)pOb] = (void*)(DWORD_PTR)m_nMapCount++;
// cause the object to serialize itself ((CObject*)pOb)->Serialize(*this);//*** } } ((CObject*)pOb)->Serialize(*this); 循线而上,你可以发现该函数最终调用的是CStroke::Serialize(CArchive&ar); 也许你可能不晓得wNullTag,dwBigClassTag等之类是何东东?看看下面它们是如何定义的: // Pointer mapping constants,in arcobj.cpp #define wNullTag ((WORD)0) // special tag indicating NULL ptrs #define wNewClassTag ((WORD)0xFFFF) // special tag indicating new CRuntimeClass #define wClassTag ((WORD)0x8000) // 0x8000 indicates class tag (OR'd) #define dwBigClassTag ((DWORD)0x80000000) // 0x8000000 indicates big class tag (OR'd) #define wBigObjectTag ((WORD)0x7FFF) // 0x7FFF indicates DWORD object tag #define nMaxMapCount ((DWORD)0x3FFFFFFE) // 0x3FFFFFFE last valid mapCount 他们不是别的,只是一些记号,比如当你写入一个CStroke类时,它首先判断CStroke以前是否出现过,若没有,则写入 wNewClassTag (0xFFFF),否则写入wClassTag+一定的offsets,表示这是与前面相同的旧类。 //MFC文档:to store the version and class information of a base class during serialization of the derived class. void CArchive::WriteClass(const CRuntimeClass* pClassRef) { ...// // make sure m_pStoreMap is initialized MapObject(NULL); // write out class id of pOb, with high bit set to indicate // new object follows // ASSUME: initialized to 0 map DWORD nClassIndex; if ((nClassIndex = (DWORD)(DWORD_PTR)(*m_pStoreMap)[(void*)pClassRef]) != 0) { // previously seen class, write out the index tagged by high bit if (nClassIndex < wBigObjectTag) *this << (WORD)(wClassTag | nClassIndex); else { *this << wBigObjectTag; *this << (dwBigClassTag | nClassIndex); } } else { // store new class *this << wNewClassTag; pClassRef->Store(*this);//***
// store new class reference in map, checking for overflow CheckCount(); (*m_pStoreMap)[(void*)pClassRef] = (void*)(DWORD_PTR)m_nMapCount++; } } 最终调用void CRuntimeClass::Store(CArchive& ar) const;来存储class information; 到这就"写"完了。下面总结一下: 我们看看本程序到底向硬盘中写了什么? 按顺序应该是:CTypedPtrList中的元素个数---〉新旧类标志---〉版本号(m_wSchema)---〉 类名称字符串中的字符个数----〉类名称(ANSI码)---〉调用其它成员的 Serialize 函数。---〉以此类推。 ////////////////////////////////////// /* 3.“读文件” */ ////////////////////////////////////// 看完了“写文件”,让我们看看“读文件”的内幕吧!“读文件”顾名思义,即当你打开一个文件时,应用程序从硬盘中将文件的数据读出的过程。 当你选中“文件”菜单中的“打开”或在工具栏中单击“打开”项时,应用程序将连续调用以下序列的函数: CWinApp::OnOpenFile()——>CDocManager::OnFileOpen()-->CWinApp::OpenDocumentFile(LPCTSTR lpszFileName)-->CDocManager::OpenDocumentFile(LPCTSTR lpszFileName)---> CSingleDocTemplate::OpenDocumentFile(LPCTSTR lpszPathName,BOOL bMakeVisible)-->
//in Doccore.cpp BOOL CDocument::OnOpenDocument(LPCTSTR lpszPathName) { ...// CFileException fe; CFile* pFile = GetFile(lpszPathName, CFile::modeRead|CFile::shareDenyWrite, &fe); if (pFile == NULL) { ReportSaveLoadException(lpszPathName, &fe, FALSE, AFX_IDP_FAILED_TO_OPEN_DOC); return FALSE; }
DeleteContents(); SetModifiedFlag(); // dirty during de-serialize
CArchive loadArchive(pFile, CArchive::load | CArchive::bNoFlushOnDelete);//*** loadArchive.m_pDocument = this; loadArchive.m_bForceFlat = FALSE; TRY { CWaitCursor wait; if (pFile->GetLength() != 0) Serialize(loadArchive); // load me*** loadArchive.Close(); ReleaseFile(pFile, FALSE); } CATCH_ALL(e) { ReleaseFile(pFile, TRUE); DeleteContents(); // remove failed contents
TRY { ReportSaveLoadException(lpszPathName, e, FALSE, AFX_IDP_FAILED_TO_OPEN_DOC); } END_TRY DELETE_EXCEPTION(e); return FALSE; } END_CATCH_ALL
SetModifiedFlag(FALSE); // start off with unmodified
return TRUE; }
--->void CMyDoc::Serialize(CArchive& ar) { if (ar.IsStoring()) { // TODO:在此添加存储代码/写入 } else { // TODO:在此添加加载代码/读出 } m_strokList.Serialize(ar); } //in list_o.cpp -->void CObList::Serialize(CArchive& ar) { ASSERT_VALID(this); CObject::Serialize(ar); if (ar.IsStoring()) { ...// } else { DWORD_PTR nNewCount = ar.ReadCount();//读入CTypedPtrList中的元素个数 CObject* newData; while (nNewCount--) { ar >> newData;//*** AddTail(newData); } } } ---〉 _AFX_INLINE CArchive& AFXAPI operator>>(CArchive& ar, CObject*& pOb) { pOb = ar.ReadObject(NULL); return ar; }
--->//in arcobj.cpp CObject* CArchive::ReadObject(const CRuntimeClass* pClassRefRequested) { ...// // attempt to load next stream as CRuntimeClass UINT nSchema; DWORD obTag; CRuntimeClass* pClassRef = ReadClass(pClassRefRequested, &nSchema, &obTag);//***
// check to see if tag to already loaded object CObject* pOb; if (pClassRef == NULL) { if (obTag > (DWORD)m_pLoadArray->GetUpperBound()) { // tag is too large for the number of objects read so far AfxThrowArchiveException(CArchiveException::badIndex, m_strFileName); }
pOb = (CObject*)m_pLoadArray->GetAt(obTag); if (pOb != NULL && pClassRefRequested != NULL && !pOb->IsKindOf(pClassRefRequested)) { // loaded an object but of the wrong class AfxThrowArchiveException(CArchiveException::badClass, m_strFileName); } } else { // allocate a new object based on the class just acquired pOb = pClassRef->CreateObject();//*** if (pOb == NULL) AfxThrowMemoryException();
// Add to mapping array BEFORE de-serializing CheckCount(); m_pLoadArray->InsertAt(m_nMapCount++, pOb);
// Serialize the object with the schema number set in the archive UINT nSchemaSave = m_nObjectSchema; m_nObjectSchema = nSchema; pOb->Serialize(*this);//*** m_nObjectSchema = nSchemaSave; ASSERT_VALID(pOb); }
return pOb; }
--->//读取CRuntimeClass信息 CRuntimeClass* CArchive::ReadClass(const CRuntimeClass* pClassRefRequested, UINT* pSchema, DWORD* pObTag) { ...// // make sure m_pLoadArray is initialized MapObject(NULL);
// read object tag - if prefixed by wBigObjectTag then DWORD tag follows DWORD obTag; WORD wTag; *this >> wTag;//***读取标志位 if (wTag == wBigObjectTag) *this >> obTag; else obTag = ((wTag & wClassTag) << 16) | (wTag & ~wClassTag);
// check for object tag (throw exception if expecting class tag) if (!(obTag & dwBigClassTag)) { if (pObTag == NULL) AfxThrowArchiveException(CArchiveException::badIndex, m_strFileName);
*pObTag = obTag; return NULL; }
CRuntimeClass* pClassRef; UINT nSchema; if (wTag == wNewClassTag) { // new object follows a new class id if ((pClassRef = CRuntimeClass::Load(*this, &nSchema)) == NULL)//*** AfxThrowArchiveException(CArchiveException::badClass, m_strFileName);
// check nSchema against the expected schema if ((pClassRef->m_wSchema & ~VERSIONABLE_SCHEMA) != nSchema) { if (!(pClassRef->m_wSchema & VERSIONABLE_SCHEMA)) { // schema doesn't match and not marked as VERSIONABLE_SCHEMA AfxThrowArchiveException(CArchiveException::badSchema, m_strFileName); } else { // they differ -- store the schema for later retrieval if (m_pSchemaMap == NULL) m_pSchemaMap = new CMapPtrToPtr; ASSERT_VALID(m_pSchemaMap); m_pSchemaMap->SetAt(pClassRef, (void*)(DWORD_PTR)nSchema); } } CheckCount(); m_pLoadArray->InsertAt(m_nMapCount++, pClassRef); } else { ...// }
// check for correct derivation if (pClassRefRequested != NULL && !pClassRef->IsDerivedFrom(pClassRefRequested)) { AfxThrowArchiveException(CArchiveException::badClass, m_strFileName); }
// store nSchema for later examination if (pSchema != NULL) *pSchema = nSchema; else m_nObjectSchema = nSchema;
// store obTag for later examination if (pObTag != NULL) *pObTag = obTag;
// return the resulting CRuntimeClass* return pClassRef; } ---> CRuntimeClass* PASCAL CRuntimeClass::Load(CArchive& ar, UINT* pwSchemaNum) // loads a runtime class description { WORD nLen; char szClassName[64];
WORD wTemp; ar >> wTemp; *pwSchemaNum = wTemp;//读入版本号 ar >> nLen;//读入类名称字符串中的字符个数
// load the class name,读入类名称 if (nLen >= _countof(szClassName) || ar.Read(szClassName, nLen*sizeof(char)) != nLen*sizeof(char)) { return NULL; } szClassName[nLen] = '/0';
// match the string against an actual CRuntimeClass CRuntimeClass* pClass = FromName(szClassName); if (pClass == NULL) { // not found, trace a warning for diagnostic purposes TRACE(traceAppMsg, 0, "Warning: Cannot load %hs from archive. Class not defined./n", szClassName); }
return pClass; } ---〉在CArchive::ReadObject函数中有pOb->Serialize(*this);即调用CStroke类的Serialize函数。以此类推。 总结:我们看看本程序到底从硬盘中读了什么? 按顺序应该是:CTypedPtrList中的元素个数---〉新旧类标志---〉版本号(m_wSchema)---〉 类名称字符串中的字符个数----〉类名称(ANSI码)---〉调用其它成员的 Serialize 函数。---〉以此类推。 由此可以看出读入数据的顺序与写入数据时的顺序完全相同。
本文深入剖析MFC中的文档序列化过程,包括写文件和读文件的具体实现细节。介绍了如何利用CArchive类和CRuntimeClass结构实现数据的持久化,并通过示例代码展示了序列化流程。
1518

被折叠的 条评论
为什么被折叠?



