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

原创作品,转载请标明:http://blog.youkuaiyun.com/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的最后写回文件
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值