1. 程式人生 > >【Cocos2d-x原始碼分析】 UserDefault如何儲存本地資料

【Cocos2d-x原始碼分析】 UserDefault如何儲存本地資料

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的最後寫回檔案