cocos引擎并没有提供一套专门资源管理解决方案,仅仅只有一个TextureCache提供了针对Texture2D的(异步)加载、缓存、清理等管理工作,我们可以参照TextureCache的思路设计一个针对常见资源的可扩展的资源管理器ResourceCache,使用引用计数原理来控制每个资源的生命周期。
UML
使用示例
#include <iostream>
#include "ResourceMgr.h"
using namespace std;
// 假设已经扩展了以下几种资源类型:
// JsonData
// Mp3Data
// Mp4Data
void main(){
// 考虑以下情景:
// 首先启动游戏,进入到场景1,然后从场景1切换到场景2,场景1、2之间共用了部分资源
// 场景1的资源列表
vector<pair<string, string>> data_scene_1{
{ "jsondata_1", JsonData::TYPE },
{ "1.mp3", Mp3Data::TYPE },
{ "1.mp4", Mp4Data::TYPE },
},
// 场景2的资源列表
data_scene2{
{ "jsondata_1", JsonData::TYPE }, // 和场景1共享的资源
{ "2.mp3", Mp3Data::TYPE },
{ "2.mp4", Mp4Data::TYPE },
}
ResourceMgr::getInstance()->addResRef(data_scene_1);
// 当前资源引用计数表:实际还没有加载场景1资源
// {
// "jsondata_1" : 1,
// "1.mp3" : 1,
// "1.mp4" : 1,
// }
// 加载当前资源引用计数表中的资源,即:jsondata_1、1.mp3、1.mp4
// callback: 加载完成后的回调
ResourceMgr::getInstance()->updateResRefAsync(callback);
ResourceMgr::getInstance()->addResRef(data_scene_2); // 加载场景2的资源
// 当前资源引用计数表:实际还没有加载场景2资源
// {
// "jsondata_1" : 2,
// "1.mp3" : 1,
// "1.mp4" : 1,
// "2.mp3" : 1,
// "2.mp4" : 1,
//}
ResourceMgr::getInstance()->removeResRef(data_scene_1); // 清除上一个场景的资源
// 当前资源引用计数表:
// {
// "jsondata_1" : 1,
// "1.mp3" : 0,
// "1.mp4" : 0,
// "2.mp3" : 1,
// "2.mp4" : 1,
// }
ResourceMgr::getInstance()->updateResRefAsync(data_scene_2);
//1、卸载引用计数=0的资源,即1.mp3、1.mp4
//2、加载引用计数!=0且还未加载的资源,即2.mp3、2.mp4
//
// 当前缓存的资源如下:
//{
// "jsondata_1" : ...,
// "2.mp3" : ...,
// "2.mp4" : ...,
//}
}
代码设计
ResourceMgr
ResourceMgr.h
#ifndef __RESOURCEMGR_H__
#define __RESOURCEMGR_H__
#include "cocos2d.h"
#include "ResourceCache.h"
/**
* 资源管理的统一(唯一)入口
*/
class ResourceMgr : public cocos2d::Ref {
public:
static ResourceMgr* getInstance();
void addResProtocol(ResourceProtocol* data);
/**
* 资源引用计数管理API
*
* 注意,有两种引用计数:
* 1、Ref引用计数
* 2、资源引用计数:ResourceMgr内部定义的引用计数,资源的引用计数+1表示该资源将在下一个场景使用,
* 资源并不会马上加载,而是在updateResourceReferenceAsync时,才真正加载。
*
*/
/**
* 告知资源管理器将要使用哪些资源(对应资源引用计数+1)
* 新增加的资源将在下一次调用updateResourceReferenceAsync的时候异步载入
*/
void addResRef(const std::vector<std::pair<std::string,std::string>>& path_type);
/**
* 告知资源管理器哪些资源不再被使用(对应资源引用计数-1)
* 当资源引用计数为0时将在下次调用updateResourceAsync的时候从内存中释放
*/
void removeResRef(const std::vector<std::string>&);
void removeUnusedResRef();
/** 查询某个资源的引用计数 */
int getResRef(const std::string& filename);
/**
* 资源引用计数 > 0,且资源未加载,则加载资源
* 资源引用计数 = 0,则清楚该资源缓存
*/
void updateResRefAsync(const std::function<void()>& callback);
void updateResRefAsyncHandler(int handler);
void unupdateResRefAsyncHandler();
private:
ResourceMgr();
virtual ~ResourceMgr();
private:
ResourceCache* _resourceCache; // 自定义资源缓存管理,实现原理参考TextureCache
/**
* 资源引用计数表
*
* key : resource full path
* value : resource reference count
*/
std::unordered_map<std::string, int> _resRefMap;
/**
* 待加载的资源
*
* key : resource full path
* value : resource type
*/
std::vector<std::pair<std::string, std::string>> _resToAdd;
int _addHandler;
int _updateHandler;
bool _isLoading;
};
#endif /* defined(__Resource__) */
ResourceMgr.cpp
#include "ResourceMgr.h"
#include "JsonData.h"
USING_NS_CC;
using namespace std;
static ResourceMgr* _resourceInstance = nullptr;
ResourceMgr* ResourceMgr::getInstance()
{
if (_resourceInstance == nullptr) {
_resourceInstance = new ResourceMgr();
}
return _resourceInstance;
}
ResourceMgr::ResourceMgr()
: _resourceCache(nullptr)
, _updateHandler(0)
, _addHandler(0)
, _isLoading(false)
{
_resourceCache = new ResourceCache();
}
ResourceMgr::~ResourceMgr()
{
CCLOG("~ResourceMgr");
unupdateResRefAsyncHandler();
}
void ResourceMgr::addResProtocol(ResourceProtocol* data)
{
_resourceCache->addResourceProtocol(data);
}
void ResourceMgr::addResRef(const std::vector<std::pair<std::string, std::string>>& path_type)
{
if (path_type.empty()) {
return;
}
for (auto iter = path_type.cbegin(); iter != path_type.cend(); iter++) {
auto fullpath = cocos2d::FileUtils::getInstance()->fullPathForFilename(iter->first);
if (!fullpath.empty()) {
auto find = _resRefMap.find(fullpath);
if (find != _resRefMap.end()) {
find->second++;
}
else {
_resRefMap.insert(std::make_pair(fullpath, 1));
_resToAdd.push_back(make_pair(fullpath, iter->second));
}
}
}
}
void ResourceMgr::removeResRef(const std::vector<std::string>& files)
{
for (auto iter = files.cbegin(); iter != files.cend(); iter++) {
auto fullpath = cocos2d::FileUtils::getInstance()->fullPathForFilename(*iter);
if (!fullpath.empty()) {
auto find = _resRefMap.find(fullpath);
if (find != _resRefMap.end() && find->second > 0) {
find->second--;
}
}
}
}
void ResourceMgr::removeUnusedResRef()
{
for (auto iter = _resRefMap.begin(); iter != _resRefMap.end();) {
if (iter->second == 0) {
iter = _resRefMap.erase(iter);
}
else {
iter++;
}
}
}
int ResourceMgr::getResRef(const std::string& file)
{
auto fullpath = cocos2d::FileUtils::getInstance()->fullPathForFilename(file);
auto find = _resRefMap.find(fullpath);
if (find != _resRefMap.end()) {
return find->second;
}
return 0;
}
void ResourceMgr::unupdateResRefAsyncHandler()
{
if (0 != _updateHandler) {
ScriptEngineManager::getInstance()->getScriptEngine()->removeScriptHandler(_updateHandler);
_updateHandler = 0;
}
}
void ResourceMgr::updateResRefAsyncHandler(int handler)
{
unupdateResRefAsyncHandler();
_updateHandler = handler;
auto callback = [this] {
#if CC_ENABLE_SCRIPT_BINDING
CommonScriptData data(_updateHandler, "", this);
ScriptEvent event(kCommonEvent, (void*)&data);
CCLOG("updateResourceReferenceAsyncHandler callback lua.");
ScriptEngineManager::getInstance()->getScriptEngine()->sendEvent(&event);
return;
#endif
};
updateResRefAsync(callback);
}
void ResourceMgr::updateResRefAsync(const std::function<void()>& callback)
{
// 第一步:清除资源,资源引用计数=0的资源。
for (auto iter = _resRefMap.begin(); iter != _resRefMap.end(); iter++) {
if (iter->second == 0) {
_resourceCache->removeResourceForKey(iter->first);
}
}
// 第二步:移除资源引用计数,=0的
removeUnusedResRef();
// 第三步:异步加载新的资源(引用计数为1的)
int* reference = new int();
*reference = _resToAdd.size();
#if COCOS2D_DEBUG
time_t* t = new time_t();
time(t);
int count = _resToAdd.size();
#endif
//资源全部加载完毕
auto completed = [reference, callback, this
#if COCOS2D_DEBUG
, t, count
#endif
]() {
if ((*reference) == 0) {
delete reference;
#if COCOS2D_DEBUG
time_t now;
time(&now);
CCLOG("we spend %f seconds to load {%d} files.", (float)difftime(now, *t), count);
delete t;
#endif
_resToAdd.clear();
_isLoading = true;
callback();
}
};
CCLOG("we need to load %d files", (int)_resToAdd.size());
if (_resToAdd.size() == 0) {
completed();
return;
}
for (auto iter = _resToAdd.begin(); iter != _resToAdd.end(); iter++) {
_resourceCache->addResourceAsync(iter->first, [&](void* data) {
(*reference)--;
if (*reference < 1) {
completed();
}
}, iter->second);
}
}
ResourceCache
ResourceCache.h
#ifndef __RESOURCECACHE_H__
#define __RESOURCECACHE_H__
#include <thread>
#include <unordered_map>
#include "cocos2d.h"
#include "ResourceProtocol.h"
class ResourceMgr;
/**
* 自定义资源缓存
*
* 不对外公开,全部private,由友元ResourceMgr代理
*/
class ResourceCache : public cocos2d::Ref {
private:
ResourceCache();
virtual ~ResourceCache();
/**
* 同步加载资源到缓存
*/
void* addResource(const std::string& filename, const std::string& type = "Texture2D");
/**
* 异步加载资源到缓存
*/
void addResourceAsync(const std::string& filename, std::function<void(void*)> callback, const std::string& type="Texture2D");
/**
* 清除所有自定义资源缓存,类似TextureCache::removeAllTextures
*/
void removeAllResources();
/**
* 清除所有当前未被使用的资源,即Ref引用计数为1,类似TextureCache::removeUnusedTexture
*/
void removeUnusedResources();
/**
* 删除指定资源
*/
bool removeResource(void* data);
bool removeResourceForKey(const std::string& key);
/**
* 注册一个自定义数据类型及解析器
*/
void addResourceProtocol(ResourceProtocol* data);
private:
/**
* 自定义资源加载完成的回调,schedule update每帧执行,也就是说是在GL thread中执行
*/
void addResourceAsyncCallback(float dt);
/**
* 在子线程中加载自定义资源
*/
void loadData();
private:
struct AsyncStruct {
AsyncStruct(const std::string& fn, std::function<void(ResourceProtocol*)> cb, const std::string& type)
:filename(fn)
, callback(cb)
, resourceType(type)
{
}
std::string filename;
std::string resourceType;
std::function<void(ResourceProtocol*)> callback;
};
typedef struct _DataInfo {
AsyncStruct* asyncStruct;
ResourceProtocol* data;
} DataInfo;
std::thread* _loadingThread;
std::queue<AsyncStruct*>* _asyncStructQueue;
std::deque<DataInfo*>* _dataInfoQueue;
std::mutex _asyncStructMutex;
std::mutex _dataInfoMutex;
std::mutex _sleepMutex;
std::condition_variable _sleepCondition;
bool _needQuit;
int _asyncRefCount;
private:
cocos2d::TextureCache *_textureCache;
/** 所有缓存数据 */
std::unordered_map<std::string, ResourceProtocol*> _resources;
/**
* 自定义资源类型模板
*/
std::map<std::string, ResourceProtocol*> _resourceTemplate;
// 此类的代理
friend ResourceMgr;
};
#endif /* defined(__ResourceCache__) */
ResourceCache.cpp
#include "ResourceCache.h"
USING_NS_CC;
ResourceCache::ResourceCache()
: _loadingThread(nullptr)
, _asyncStructQueue(nullptr)
, _dataInfoQueue(nullptr)
, _needQuit(false)
, _asyncRefCount(0)
, _textureCache(Director::getInstance()->getTextureCache())
{
}
ResourceCache::~ResourceCache()
{
for (auto iter = _resources.begin(); iter != _resources.end(); ++iter) {
iter->second->release();
}
for (auto iter = _resourceTemplate.begin(); iter != _resourceTemplate.end(); iter++) {
iter->second->release();
}
}
void* ResourceCache::addResource(const std::string& filename, const std::string& type)
{
CCASSERT(!filename.empty(), "filename cann't be nil.");
void* resource = nullptr;
if (type == "Texture2D") {
resource = _textureCache->addImage(filename);
}
else {
//全路径key
std::string fullpath = cocos2d::FileUtils::getInstance()->fullPathForFilename(filename);
if (fullpath.empty()) {
return nullptr;
}
//是否已加载
auto iter = _resources.find(fullpath);
if (iter != _resources.end()) {
resource = iter->second;;
}
if (!resource) {
do {
CCLOG("add resource date type: %s", type.c_str());
auto iter1 = _resourceTemplate.find(type);
if (iter1 != _resourceTemplate.end()) {
ResourceProtocol* data = iter1->second->cloneEmpty();
if (!data->loadWithFile(filename)) {
CC_SAFE_RELEASE(data);
CCLOG("ResouceDataDelegate is error: %s", iter1->first.c_str());
break;
}
_resources.insert(std::make_pair(fullpath, data));
resource = data;
}
}
while (0);
}
}
return resource;
}
void ResourceCache::addResourceAsync(const std::string& filename, std::function<void(void*)> callback, const std::string& type)
{
CCASSERT(!filename.empty(), "filename cann't be nil.");
if (type == "Texture2D") {
auto func = [this, filename, callback, type](cocos2d::Texture2D* tex) {
callback(tex);
};
_textureCache->addImageAsync(filename, func);
}
else {
ResourceProtocol* data = nullptr;
std::string fullpath = FileUtils::getInstance()->fullPathForFilename(filename);
auto iter = _resources.find(fullpath);
if (iter != _resources.end()) {
data = iter->second;
}
if (data != nullptr) {
callback(data);
return;
}
//lazy init
if (_asyncStructQueue == nullptr) {
_asyncStructQueue = new std::queue<AsyncStruct*>();
_dataInfoQueue = new std::deque<DataInfo*>();
_loadingThread = new std::thread(&ResourceCache::loadData, this);
_needQuit = false;
}
if (_asyncRefCount == 0) {
Director::getInstance()->getScheduler()->schedule(schedule_selector(ResourceCache::addResourceAsyncCallback), this, 0, false);
}
++_asyncRefCount;
AsyncStruct* async = new AsyncStruct(fullpath, callback, type);
_asyncStructMutex.lock();
_asyncStructQueue->push(async);
_asyncStructMutex.unlock();
_sleepCondition.notify_one();
}
}
void ResourceCache::addResourceAsyncCallback(float dt)
{
std::deque<DataInfo*>* datasQueue = _dataInfoQueue;
_dataInfoMutex.lock();
if (_dataInfoQueue->empty()) {
_dataInfoMutex.unlock();
}
else {
DataInfo* dataInfo = datasQueue->front();
datasQueue->pop_front();
_dataInfoMutex.unlock();
AsyncStruct* asyncStruct = dataInfo->asyncStruct;
ResourceProtocol* data = dataInfo->data;
const std::string& filename = asyncStruct->filename;
if (data) {
_resources.insert(std::make_pair(filename, data));
data->retain();
data->autorelease();
}
else {
auto iter = _resources.find(asyncStruct->filename);
if (iter != _resources.end()) {
data = iter->second;
}
}
asyncStruct->callback(data);
delete asyncStruct;
delete dataInfo;
--_asyncRefCount;
if (0 == _asyncRefCount) {
Director::getInstance()->getScheduler()->unschedule(schedule_selector(ResourceCache::addResourceAsyncCallback), this);
}
}
}
void ResourceCache::loadData()
{
AsyncStruct* asyncStruct = nullptr;
while (true) {
std::queue<AsyncStruct*>* pQueue = _asyncStructQueue;
_asyncStructMutex.lock();
if (pQueue->empty()) {
_asyncStructMutex.unlock();
if (_needQuit) {
break;
}
else {
std::unique_lock<std::mutex> lk(_sleepMutex);
_sleepCondition.wait(lk);
continue;
}
}
else {
asyncStruct = pQueue->front();
pQueue->pop();
_asyncStructMutex.unlock();
}
ResourceProtocol* data = nullptr;
bool loaded = false;
auto iter = _resources.find(asyncStruct->filename);
if (iter == _resources.end()) {
_dataInfoMutex.lock();
DataInfo* dataInfo;
size_t pos = 0;
size_t infoSize = _dataInfoQueue->size();
for (; pos < infoSize; pos++) {
dataInfo = (*_dataInfoQueue)[pos];
if (dataInfo->asyncStruct->filename.compare(asyncStruct->filename)) {
break;
}
}
_dataInfoMutex.unlock();
if (infoSize == 0 || pos < infoSize) {
loaded = true;
}
}
if (loaded) {
const std::string & filename = asyncStruct->filename;
auto cloneData = _resourceTemplate.find(asyncStruct->resourceType);
if (cloneData != _resourceTemplate.end()) {
data = cloneData->second->cloneEmpty();
if (!data->loadWithFile(filename)) {
delete data;
CCLOG("load file: %s fail.", filename.c_str());
continue;
}
CCLOG("load file: %s success.", filename.c_str());
}
else {
CCLOG("load file: %s fail. no parser.", filename.c_str());
continue;
}
}
DataInfo* dataInfo = new DataInfo();
dataInfo->asyncStruct = asyncStruct;
dataInfo->data = data;
_dataInfoMutex.lock();
_dataInfoQueue->push_back(dataInfo);
_dataInfoMutex.unlock();
}
}
void ResourceCache::removeAllResources()
{
for (auto it = _resources.begin(); it != _resources.end(); ++it) {
(it->second)->release();
}
_resources.clear();
_textureCache->removeAllTextures();
}
void ResourceCache::removeUnusedResources()
{
for (auto it = _resources.cbegin(); it != _resources.cend(); /* nothing */) {
ResourceProtocol *tex = it->second;
if (tex->getReferenceCount() == 1) {
CCLOG("GX: ResourceCache: removing unused data: %s", it->first.c_str());
tex->release();
_resources.erase(it++);
}
else {
++it;
}
}
_textureCache->removeUnusedTextures();
}
bool ResourceCache::removeResource(void* data)
{
return false;
}
bool ResourceCache::removeResourceForKey(const std::string& key)
{
auto it = _resources.find(key);
if (it == _resources.end()) {
std::string fullpath = FileUtils::getInstance()->fullPathForFilename(key);
it = _resources.find(fullpath);
}
if (it == _resources.end()) {
_textureCache->removeTextureForKey(key);
return true;
}
else {
if (!_resources.empty()) {
(it->second)->release();
_resources.erase(it);
return true;
}
}
return false;
}
void ResourceCache::addResourceProtocol(ResourceProtocol* data)
{
if (_resourceTemplate.find(data->getType()) == _resourceTemplate.end()) {
_resourceTemplate.insert(std::make_pair(data->getType(), data));
}
}
ResourceProtocol
ResourceProtocol.h
#ifndef __RESOURCEPROTOCOL_H__
#define __RESOURCEPROTOCOL_H__
#include "cocos2d.h"
/**
* 自定义数据类型
*/
class ResourceProtocol : public cocos2d::Ref {
public:
ResourceProtocol() {};
virtual ~ResourceProtocol() {};
public:
virtual const std::string& getType() const = 0;
//返回一个空数据的实例,用于创建数据实例
virtual ResourceProtocol* cloneEmpty() const = 0;
//必须实现加载数据到内存方式
virtual bool loadWithFile(const std::string& filename) = 0;
};
#endif
JsonData
JsonData.h
#ifndef __JSONDATA_H__
#define __JSONDATA_H__
#include "ResourceProtocol.h"
#include "json\rapidjson.h"
class JsonData : public ResourceProtocol {
public:
JsonData();
~JsonData();
const std::string& getType() const override
{
return TYPE;
};
ResourceProtocol* cloneEmpty() const override;
bool loadWithFile(const std::string& filename) override;
public:
static std::string TYPE;
rapidjson::Document data;
};
#endif /* defined(__JsonData__) */
JsonData.cpp
#include "JsonData.h"
using namespace std;
USING_NS_CC;
std::string JsonData::TYPE = "JsonData";
JsonData::JsonData()
{
}
JsonData::~JsonData()
{
}
ResourceProtocol* JsonData::cloneEmpty() const
{
return new JsonData();
}
bool JsonData::loadWithFile(const std::string& filename)
{
bool bRet = false;
do {
std::string jsonpath = cocos2d::FileUtils::getInstance()->fullPathForFilename(filename);
std::string contentStr = cocos2d::FileUtils::getInstance()->getStringFromFile(jsonpath);
data.Parse<0>(contentStr.c_str());
CC_BREAK_IF(data.HasParseError());
bRet = true;
}
while (0);
return bRet;
}