分享
 
 
 

MFC技术内幕系列之(五)---MFC文档序列化内幕

王朝vc·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

////////////////////////////////////////////////////////////////////////////////////

/********* 文章系列:MFC技术内幕系列***********/

/************MFC技术内幕系列之(五)***********/

/*********文章题目:MFC文档序列化内幕**********/

/* Copyright(c)2002 bigwhite */

/* All rights Reserved */

/ ***********关键字:文档序列化 ************** /

/* 时间:2002.7.23 */

/* 注释:本文所涉及的程序源代码均在Microsoft */

/ Visual Studio.Net Enterprise Architect Edition /

/* 开发工具包提供的源代码中 */

////////////////////////////////////////////////////////////////////////////////////////////////

引言:

引入“文档序列化”的概念会让许多人感到迷惑,什么是“文档序列化”?其实说白了就是将数据(广义上的概念)从硬盘中读出或将数据写入硬盘中。其稍正规的定义可以看看MFC文档如下:

The CArchive class allows you to save a complex network of objects in a permanent binary form (usually disk storage) that persists after those objects are deleted. Later you can load the objects from persistent storage, reconstituting them in memory. This process of making data persistent is called "serialization."

在MFC中,CArchive类和CRuntimeClass结构以及一些神秘地宏共同完成“文档序列化”这一工作,当然了我们也不能忘记CFile类以及它的那些派生类的功劳。

正文:

“文档序列化”显然可以分成两个部分“写文件”和“读文件”。我在本文中也将从这两个方面来为你挖掘文档序列化的奥秘。

///////////////////////////////////////////////

/* 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 函数。---〉以此类推。

由此可以看出读入数据的顺序与写入数据时的顺序完全相同。

/////////////////////////////////////

/* 4.结局 */

/////////////////////////////////////

至此,MFC技术内幕系列文章都已结束,文章中肯定有很多纰漏和错误,希望读者们批评指点。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有