【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:
其中,UserDefault在实现上使用了单例模式,getInstance方法返回唯一的实例。setXXXForKey用来设置指定类型的数据,getXXXForKey用来获取指定类型的数据。这几个接口简单易懂,那接下来,我们就到源码里面看看UserDefault是如何保存本地数据的。
代码2:
代码2是getInstance的实现代码,里面出现了“XMLFilePath”和“XMLFile”字样,我们是不是可以大胆地猜测:UserDefault会不会将数据保存在XML文件中?带着这个猜测,我们继续往下分析。在代码2中,_userDefault的定义如下:
当用户第一次调用getInstance函数时候,! _userDefault判断必然为真,所以执行了if语句里面的代码。其实这就是单例模式的典型实现方式。Cocos2d-x采用了“懒汉式”的单例模式实现,当用户真正需要使用时再进行初始化。该初始化过程主要做了下面三件事:
initXMLFilePath()
isXMLFileExist()
createXMLFile()
我们先来看看initXMLFilePath函数的实现:
代码3:
在代码3中,我们可以看到,initXMLFilePath函数主要功能就是初始化文件的存放路径。文件的名字XML_FILE_NAME被定义为:
到这里我们是不是几乎可以确定,UserDefault就是利用xml文件来保存本地数据,而且这个文件的名称就叫“UserDefault.xml”!那这个文件又被存放在哪里呢?这又依赖于FileUtils类来根据不同的平台来确定不同的目录。关于这一点,大家可以看看我的另一篇博客【Cocos2d-x源码分析】 FileUtils如何跨平台查找文件,在这里就不再一一分析了。
对于_filePath 的值,我们可以将其输出,看看它具体的值。我在win32中调用UserDefault::getInstance()->getXMLFilePath()函数输出如下:
接下来isXMLFileExist方法判断_filePath 路径上的xml文件是否存在,如果不存在则调用createXMLFile方法创建一个新的xml文件。
代码4:
在代码4中,我们可以捕捉到两个重要信息:
一是Cocos2d-x使用tinyxml2来操作xml文件。由于本文只是分析UserDefault的实现机制,对于tinyxml2就不展开介绍,需要进一步了解的童鞋可以移步官网或者GitHub
二是createXMLFile函数创建了一个xml文件并设置了头节点,然后保存在_filePath指定的路径上。我们找到该xml文件,可以看到初始化后的xml文件内容如下:
getStringForKey的实现如下:
代码5:
在代码5中,我们可以看到getStringForKey(const char* pKey)实际上调用了getStringForKey(const char* pKey, const std::string & defaultValue)来实现数据保存,这对于其他类型的getter方法也差不多如此。getStringForKey方法中最重要的是getXMLNodeForKey函数。从它的命名我们可以看出,该函数在xml文件中查找指定key的xml结点然后返回,这样getStringForKey方法就直接从目标结点中读取保存的数据然后返回。我们进一步跟踪,看看getXMLNodeForKey函数的实现。
代码6:
从代码5中,我们可以看到getXMLNodeForKey的工作就是将xml文件读进内存、解析、遍历节点直至找到参数key对应的目标结点。这里涉及tinyxml2较多的xml操作函数,感兴趣的童鞋可以自动gg一下。
不知道你有没有注意到getXMLNodeForKey并不是UserDefault的成员函数,而是被定义为static函数,这样它的可见性就被限制在仅该文件可见,作者给出了这样做的理由:
接下来,再来看看setStringForKey函数的实现。
代码7:
不用解释,我们继续追踪setValueForKey
代码8:
在代码8中,我们可以根据注释来阅读这段代码。该函数主要做了以下事情:
在xml文件中查找参数key指定的结点
如果找到目标结点,直接修改对应的值;如果没有找到目标结点,则创建一个新结点并链接到xml字符串中。
保存修改后的文件,释放资源
总结:
UserDefault类通过XML文件来将游戏数据保存本地,该文件名称为UserDefault.xml。
每次调用setXXXForKey和getXXXForKey函数时,UserDefault总是需要经历读入解析UserDefault.xml文件,查找参数key指定的结点,进行读/写操作,保存文件(如果前面是写操作) 等步骤。
UserDefault虽然提供了flush函数,但是该函数并未进行任何操作。UserDefault在每次的setXXXForKey的最后写回文件
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的最后写回文件
相关文章推荐
- 【玩转cocos2d-x之四十】怎样在Cocos2d-x 3.0中使用opengl shader?
- cocos2d-x 3.1 集成 云风pbc
- Cocos2d-x 常用特效 Effect API
- Cocos2d-x常用动作 Action API
- cocos2dx 3.3 AssetsManager测试更新
- cocos2d-x 3.0 新特性样例
- CocosCreator你又从新燃起我对Cocos的激情!
- CocosCreator你又从新燃起我对Cocos的激情!
- 如何使用cygwin去编译cocos2dx项目中的C++文件
- 关于cocos2dx的C++调用创建项目
- 每天一点点----项目中關鍵字 “ collectgarbage”
- 【COCOS CREATOR 系列教程之四】基于0.7.1先简单制作一个PAGEVIEW
- 【COCOS CREATOR 系列教程之三】PREFAB讲解&CC项目如何多开与分享
- 【COCOS CREATOR 系列教程之三】PREFAB讲解&CC项目如何多开与分享
- quick-cocos2dx scheduler.scheduleGlobal坑
- COCOS学习笔记--骨骼动画
- 王楠首次讲述Cocos Creator背后的故事
- 王楠首次讲述Cocos Creator背后的故事
- Cocos2d-x3.3RC0的Android编译Activity启动流程分析
- cocos2d-x 3.3 CC_CALLBACK_0 std::bind与std::function