您的位置:首页 > 移动开发 > Cocos引擎

【Cocos2d-x源码分析】 UserDefault如何保存本地数据

2016-02-06 17:24 796 查看
原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/50580793

Cocos2d-x提供了UserDefault类来在本地保存简单的游戏数据。今天我们的目标就是分析UserDefault是如何工作的。

本文的分析的是Cocosd2-x 3.8版本的源码,使用Vistual Studio2013。

1、初探UserDefualt

熟悉Coco2d-x的童鞋应该都知道,UserDefault类主要提供了以下接口来保存数据。

代码1:

// 获取bool类型数据
bool    getBoolForKey(const char* key);
virtual bool getBoolForKey(const char* key, bool defaultValue);
// 获取int类型数据
int     getIntegerForKey(const char* key);
virtual int getIntegerForKey(const char* key, int defaultValue);
// 获取float类型数据
float    getFloatForKey(const char* key);
virtual float getFloatForKey(const char* key, float defaultValue);
// 获取double类型数据
double  getDoubleForKey(const char* key);
virtual double getDoubleForKey(const char* key, double defaultValue);
// 获取string类型数据
std::string getStringForKey(const char* key);
virtual std::string getStringForKey(const char* key, const std::string & defaultValue);
// 获取Data类型数据,从CCData.h中我们可以看到Data其实保存的是
// unsigned char* _bytes类型数据
Data getDataForKey(const char* key);
virtual Data getDataForKey(const char* key, const Data& defaultValue);

// 获取bool类型数据
virtual void setBoolForKey(const char* key, bool value)
// 获取int类型数据
virtual void setIntegerForKey(const char* key, int value);
// 获取float类型数据
virtual void setFloatForKey(const char* key, float value);
// 获取double类型数据
virtual void setDoubleForKey(const char* key, double value);
// 获取string类型数据
virtual void setStringForKey(const char* key, const std::string & value);
// 获取Data类型数据
virtual void setDataForKey(const char* key, const Data& value);

static UserDefault* getInstance();


其中,UserDefault在实现上使用了单例模式,getInstance方法返回唯一的实例。setXXXForKey用来设置指定类型的数据,getXXXForKey用来获取指定类型的数据。这几个接口简单易懂,那接下来,我们就到源码里面看看UserDefault是如何保存本地数据的。

2、UserDefault::getInstance()实现

首先,我们肯定要先看看UserDefault是如何初始化的,我们找到UserDefault::getInstance()函数。

代码2:

UserDefault* UserDefault::getInstance()
{
if (!_userDefault)
{
initXMLFilePath();

// only create xml file one time
// the file exists after the program exit
if ((!isXMLFileExist()) && (!createXMLFile()))
{
return nullptr;
}

_userDefault = new (std::nothrow) UserDefault();
}

return _userDefault;
}


代码2是getInstance的实现代码,里面出现了“XMLFilePath”和“XMLFile”字样,我们是不是可以大胆地猜测:UserDefault会不会将数据保存在XML文件中?带着这个猜测,我们继续往下分析。在代码2中,_userDefault的定义如下:

UserDefault* UserDefault::_userDefault = nullptr;


当用户第一次调用getInstance函数时候,! _userDefault判断必然为真,所以执行了if语句里面的代码。其实这就是单例模式的典型实现方式。Cocos2d-x采用了“懒汉式”的单例模式实现,当用户真正需要使用时再进行初始化。该初始化过程主要做了下面三件事:

initXMLFilePath()

isXMLFileExist()

createXMLFile()

我们先来看看initXMLFilePath函数的实现:

代码3:

void UserDefault::initXMLFilePath()
{
if (! _isFilePathInitialized)
{
_filePath += FileUtils::getInstance()->getWritablePath() + XML_FILE_NAME;
_isFilePathInitialized = true;
}
}


在代码3中,我们可以看到,initXMLFilePath函数主要功能就是初始化文件的存放路径。文件的名字XML_FILE_NAME被定义为:

#define XML_FILE_NAME "UserDefault.xml"


到这里我们是不是几乎可以确定,UserDefault就是利用xml文件来保存本地数据,而且这个文件的名称就叫“UserDefault.xml”!那这个文件又被存放在哪里呢?这又依赖于FileUtils类来根据不同的平台来确定不同的目录。关于这一点,大家可以看看我的另一篇博客【Cocos2d-x源码分析】 FileUtils如何跨平台查找文件,在这里就不再一一分析了。

对于_filePath 的值,我们可以将其输出,看看它具体的值。我在win32中调用UserDefault::getInstance()->getXMLFilePath()函数输出如下:

C:/Users/fred/AppData/Local/CocosTest/UserDefault.xml


接下来isXMLFileExist方法判断_filePath 路径上的xml文件是否存在,如果不存在则调用createXMLFile方法创建一个新的xml文件。

代码4:

// create new xml file
bool UserDefault::createXMLFile()
{
bool bRet = false;
tinyxml2::XMLDocument *pDoc = new tinyxml2::XMLDocument();
if (nullptr==pDoc)
{
return false;
}
tinyxml2::XMLDeclaration *pDeclaration = pDoc->NewDeclaration(nullptr);
if (nullptr==pDeclaration)
{
return false;
}
pDoc->LinkEndChild(pDeclaration);
tinyxml2::XMLElement *pRootEle = pDoc->NewElement(USERDEFAULT_ROOT_NAME);
if (nullptr==pRootEle)
{
return false;
}
pDoc->LinkEndChild(pRootEle);
bRet = tinyxml2::XML_SUCCESS == pDoc->SaveFile(FileUtils::getInstance()->getSuitableFOpen(_filePath).c_str());

if(pDoc)
{
delete pDoc;
}

return bRet;
}


在代码4中,我们可以捕捉到两个重要信息:

一是Cocos2d-x使用tinyxml2来操作xml文件。由于本文只是分析UserDefault的实现机制,对于tinyxml2就不展开介绍,需要进一步了解的童鞋可以移步官网或者GitHub

二是createXMLFile函数创建了一个xml文件并设置了头节点,然后保存在_filePath指定的路径上。我们找到该xml文件,可以看到初始化后的xml文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<userDefaultRoot/>


3、setXXXForKey和getXXXForKey的实现

通过前面的分析,我们知道UserDefault通过xml文件来保存本地数据。如果你在平时编程时有使用过xml文件,是不是很容易猜到setXXXForKey和getXXXForKey是如何实现的?没错,其实就是创建 or 查找结点,然后读写该结点。由于不同类型的setXXXForKey和getXXXForKey方法有很大的相似性,这里我们就挑比较典型的setStringForKey和getStringForKey方法来讲解一下。

getStringForKey的实现如下:

代码5:

std::string UserDefault::getStringForKey(const char* pKey)
{
return getStringForKey(pKey, "");
}

string UserDefault::getStringForKey(const char* pKey, const std::string & defaultValue)
{
const char* value = nullptr;
tinyxml2::XMLElement* rootNode;
tinyxml2::XMLDocument* doc;
tinyxml2::XMLElement* node;
node =  getXMLNodeForKey(pKey, &rootNode, &doc);
// find the node
if (node && node->FirstChild())
{
value = (const char*)(node->FirstChild()->Value());
}

string ret = defaultValue;

if (value)
{
ret = string(value);
}

if (doc) delete doc;

return ret;
}


在代码5中,我们可以看到getStringForKey(const char* pKey)实际上调用了getStringForKey(const char* pKey, const std::string & defaultValue)来实现数据保存,这对于其他类型的getter方法也差不多如此。getStringForKey方法中最重要的是getXMLNodeForKey函数。从它的命名我们可以看出,该函数在xml文件中查找指定key的xml结点然后返回,这样getStringForKey方法就直接从目标结点中读取保存的数据然后返回。我们进一步跟踪,看看getXMLNodeForKey函数的实现。

代码6:

static tinyxml2::XMLElement* getXMLNodeForKey(const char* pKey, tinyxml2::XMLElement** rootNode, tinyxml2::XMLDocument **doc)
{
tinyxml2::XMLElement* curNode = nullptr;

// check the key value
if (! pKey)
{
return nullptr;
}

do
{
tinyxml2::XMLDocument* xmlDoc = new tinyxml2::XMLDocument();
*doc = xmlDoc;

std::string xmlBuffer = FileUtils::getInstance()->getStringFromFile(UserDefault::getInstance()->getXMLFilePath());

if (xmlBuffer.empty())
{
CCLOG("can not read xml file");
break;
}
xmlDoc->Parse(xmlBuffer.c_str(), xmlBuffer.size());

// get root node
*rootNode = xmlDoc->RootElement();
if (nullptr == *rootNode)
{
CCLOG("read root node error");
break;
}
// find the node
curNode = (*rootNode)->FirstChildElement();
while (nullptr != curNode)
{
const char* nodeName = curNode->Value();
if (!strcmp(nodeName, pKey))
{
break;
}

curNode = curNode->NextSiblingElement();
}
} while (0);

return curNode;
}


从代码5中,我们可以看到getXMLNodeForKey的工作就是将xml文件读进内存、解析、遍历节点直至找到参数key对应的目标结点。这里涉及tinyxml2较多的xml操作函数,感兴趣的童鞋可以自动gg一下。

不知道你有没有注意到getXMLNodeForKey并不是UserDefault的成员函数,而是被定义为static函数,这样它的可见性就被限制在仅该文件可见,作者给出了这样做的理由:

/**
* define the functions here because we don't want to
* export xmlNodePtr and other types in "CCUserDefault.h"
*/


接下来,再来看看setStringForKey函数的实现。

代码7:

void UserDefault::setStringForKey(const char* pKey, const std::string & value)
{
// check key
if (! pKey)
{
return;
}

setValueForKey(pKey, value.c_str());
}


不用解释,我们继续追踪setValueForKey

代码8:

static void setValueForKey(const char* pKey, const char* pValue)
{
tinyxml2::XMLElement* rootNode;
tinyxml2::XMLDocument* doc;
tinyxml2::XMLElement* node;
// check the params
if (! pKey || ! pValue)
{
return;
}
// find the node
node = getXMLNodeForKey(pKey, &rootNode, &doc);
// if node exist, change the content
if (node)
{
if (node->FirstChild())
{
node->FirstChild()->SetValue(pValue);
}
else
{
tinyxml2::XMLText* content = doc->NewText(pValue);
node->LinkEndChild(content);
}
}
else
{
if (rootNode)
{
tinyxml2::XMLElement* tmpNode = doc->NewElement(pKey);//new tinyxml2::XMLElement(pKey);
rootNode->LinkEndChild(tmpNode);
tinyxml2::XMLText* content = doc->NewText(pValue);//new tinyxml2::XMLText(pValue);
tmpNode->LinkEndChild(content);
}
}

// save file and free doc
if (doc)
{
doc->SaveFile(FileUtils::getInstance()->getSuitableFOpen(UserDefault::getInstance()->getXMLFilePath()).c_str());
delete doc;
}
}


在代码8中,我们可以根据注释来阅读这段代码。该函数主要做了以下事情:

在xml文件中查找参数key指定的结点

如果找到目标结点,直接修改对应的值;如果没有找到目标结点,则创建一个新结点并链接到xml字符串中。

保存修改后的文件,释放资源

总结:

UserDefault类通过XML文件来将游戏数据保存本地,该文件名称为UserDefault.xml。

每次调用setXXXForKey和getXXXForKey函数时,UserDefault总是需要经历读入解析UserDefault.xml文件,查找参数key指定的结点,进行读/写操作,保存文件(如果前面是写操作) 等步骤。

UserDefault虽然提供了flush函数,但是该函数并未进行任何操作。UserDefault在每次的setXXXForKey的最后写回文件
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: