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