#Cocos2dx手游开发#11重构Lua端UserDefaul
序言
Cocos2dx引擎为我们提供了 cc.UserDefault 类,用于本地数据存储。在C++端的UserDefault类提供了6种数据类型的读写接口, 每种类型对应有读接口和写接口,其中的读接口还有一个重载。
这需要记住相当多的API,是一个记忆的负担。我们的问题是:
能不能有更简洁的接口?
于是,基于此问题,在不改变UserDefault类的接口基础上,在Lua脚本中增加一个新类 ud ,用于提供更简洁的key-value数据结构的存储解决方案。
关于UserDefault类的源码分析,可请参见此文章
#Cocos2dx+Lua源码#UserDefault类
大纲
本文将从以下要点进行说明
- 我们希望以怎样的方式在本地读写数据;
- 解决方案的设计;
- 解决方案的实现;
- 测试API接口;
1. 我们希望以怎样的方式在本地读写数据
我们希望是仅仅使用2个接口来实现我们的读写操作:
写入操作
ud:setValueForKey(key, value)
我们要求
- key是string类型的非空字符串
- value可以是nil、boolean、number、string四种类型,当value是nil类型时,我们希望这是一个delete操作
- 我们不希望写入过于复杂的数据类型,导致系统复杂度的增加。
读取操作
ud:getValueForKey(key, default)
我们要求
- key是string类型的非空字符串
- 当key有存储数据的时候,返回存储的数据
- 当key没有存储数据的时候,返回default
2. 解决方案的设计
我们假设关键字
key = "TestKey"
我们为其分配两个新的关键字
type_key = "TestKey_TYPE_KEY"
和
value_key = "TestKey_VALUE_KEY"
写入操作
当我们调用
ud:setValueForKey("TestKey", "John")
xml表中,会出现这样的数据结构
<?xml version="1.0" encoding="UTF-8"?>
<userDefaultRoot>
<TestKey_TYPE_KEY>S</TestKey_TYPE_KEY>
<TestKey_VALUE_KEY>John</TestKey_VALUE_KEY>
</userDefaultRoot>
其中
TestKey_TYPE_KEY节点存储的S 标记为String类型
TestKey_VALUE_KEY节点存储的这个类型对应的值John
当我们调用
ud:setValueForKey("TestKey", false)
xml表中,会出现这样的数据结构
<?xml version="1.0" encoding="UTF-8"?>
<userDefaultRoot>
<TestKey_TYPE_KEY>B</TestKey_TYPE_KEY>
<TestKey_VALUE_KEY>false</TestKey_VALUE_KEY>
</userDefaultRoot>
其中
TestKey_TYPE_KEY节点存储的B 标记为Bool类型
TestKey_VALUE_KEY节点存储的这个类型对应的值false
当我们调用
ud:setValueForKey("TestKey", 99)
xml表中,会出现这样的数据结构
<?xml version="1.0" encoding="UTF-8"?>
<userDefaultRoot>
<TestKey_TYPE_KEY>I</TestKey_TYPE_KEY>
<TestKey_VALUE_KEY>99</TestKey_VALUE_KEY>
</userDefaultRoot>
其中
TestKey_TYPE_KEY节点存储的I 标记为Integer类型
TestKey_VALUE_KEY节点存储的这个类型对应的值99
当我们调用
ud:setValueForKey("TestKey", 88.88)
xml表中,会出现这样的数据结构
<?xml version="1.0" encoding="UTF-8"?>
<userDefaultRoot>
<TestKey_TYPE_KEY>D</TestKey_TYPE_KEY>
<TestKey_VALUE_KEY>88.88</TestKey_VALUE_KEY>
</userDefaultRoot>
其中
TestKey_TYPE_KEY节点存储的D 标记为Double类型
TestKey_VALUE_KEY节点存储的这个类型对应的值88.88
当我们调用
ud:setValueForKey("TestKey", nil)
xml表中
TestKey_TYPE_KEY节点和TestKey_VALUE_KEY节点都将被删除。
读取操作
当我们调用
ud:getValueForKey("TestKey", default)
若xml表中 有 TestKey_TYPE_KEY节点,那么它一定是合法的数据,一定能够取到TestKey_VALUE_KEY的正确值。
若xml表中 没有 TestKey_TYPE_KEY节点,则返回default值
3. 解决方案的实现
-- 首先定义两个key值的转换函数
local function __typeKey(key)
return string.format("%s_TYPE_KEY", key)
end
local function __valueKey(key)
return string.format("%s_VALUE_KEY", key)
end
-- 再定义一个从判断其存储数据类型的接口
local function __cppType(value)
local lua_value_type = type(value)
local cpp_value_type
if lua_value_type == "string" then
cpp_value_type = "S"
elseif lua_value_type == "boolean" then
cpp_value_type = "B"
else
assert(lua_value_type == "number")
if value%1 == 0 then
cpp_value_type = "I"
else
cpp_value_type = "D"
end
end
return cpp_value_type
end
-- 定义类
local XXUserDefault = class(" XXUserDefault")
-- 定义类的写入接口
function XXUserDefault:setValueForKey(key, value)
local key_type = type(key)
local value_type = type(value)
assert(key ~= "" and key_type == "string")
if (value_type == "string" or value_type == "number" or value_type == "boolean") then
local _type_key = __typeKey(key)
local _value_key = __valueKey(key)
local _cpp_value_type = __cppType(value)
local _record_cpp_value_type = cc.UserDefault:getInstance():getStringForKey(_type_key)
if _record_cpp_value_type == "" then
-- 空的, 说明从来没有赋值过
cc.UserDefault:getInstance():setStringForKey(_type_key, _cpp_value_type)
_record_cpp_value_type = _cpp_value_type
end
if _record_cpp_value_type ~= _cpp_value_type then
-- 两个类型不一样
cc.UserDefault:getInstance():setStringForKey(_type_key, _cpp_value_type)
end
if _cpp_value_type == "S" then
cc.UserDefault:getInstance():setStringForKey(_value_key, value)
elseif _cpp_value_type == "B" then
cc.UserDefault:getInstance():setBoolForKey(_value_key, value)
elseif _cpp_value_type == "I" then
cc.UserDefault:getInstance():setIntegerForKey(_value_key, value)
elseif _cpp_value_type == "D" then
cc.UserDefault:getInstance():setDoubleForKey(_value_key, value)
end
elseif (value_type == "nil") then
-- 清空工作
cc.UserDefault:getInstance():deleteValueForKey(__typeKey(key))
cc.UserDefault:getInstance():deleteValueForKey(__valueKey(key))
end
end
-- 定义类的读取接口
function XXUserDefault:getValueForKey(key, default)
assert(type(key) == "string" and key ~= "", "[ERROR] key must be of type string and not empty!")
local _cpp_value_type = cc.UserDefault:getInstance():getStringForKey(__typeKey(key))
if _cpp_value_type == "S" then
return cc.UserDefault:getInstance():getStringForKey(__valueKey(key))
elseif _cpp_value_type == "B" then
return cc.UserDefault:getInstance():getBoolForKey(__valueKey(key))
elseif _cpp_value_type == "I" then
return cc.UserDefault:getInstance():getIntegerForKey(__valueKey(key))
elseif _cpp_value_type == "D" then
return cc.UserDefault:getInstance():getDoubleForKey(__valueKey(key))
else
assert(_cpp_value_type == "")
return default
end
end
-- xx.convert.singleton是将类转化为单例的函数
return xx.convert.singleton(XXUserDefault)
4. 测试API接口
我们需要测试在TestKey对应各种取值情况下,附带各种默认参数时,返回值是否正确。
-- 定义打印帮助类和基础的数据类型
local convert_ret_to_print_string = function(ret)
if type(ret) == "string" then
return ("'"..ret.."'")
end
return tostring(ret)
end
local function printRet(ret)
print("type of ret:", type(ret), " value of ret:", convert_ret_to_print_string(ret))
end
local function printJudge(bSame)
print("----> ", bSame == true and "OK" or "ERROR")
end
local test_list = {
nil,
false,
true,
100,
88.88,
"",
"default",
}
local function test(origin_value)
for i = 1, 7 do
local default_value = test_list[i]
local ret = xx.ud:getValueForKey('TestKey', default_value)
if origin_value ~= nil then
if ret ~= origin_value then
return false
end
else
if ret ~= default_value then
return false
end
end
end
return true
end
local function test_all()
for j=1, 7 do
local _value = test_list[i]
xx.ud:setValueForKey('TestKey', _value)
if not test(_value) then
return false
end
end
return true
end
printJudge(test_all())
——>
----> OK
测试通过!
总结
我们使用了一种更加优雅的访问方式,虽然增加了本地数据存储量,但由于它并非是一种频繁读写的数据库,且存储量并不会很大,于是,追求访问方式的优雅度,是我们最关注的,对于这样的结果,我个人表示很满意。