在cocos中,一个Texture2D管理一个纹理(显存中),Texture2D负责纹理的创建、删除。纹理在被glTexImage2D上传至GPU内存后将一直驻留,我们需要小心进行管理,一方面,纹理在使用之后可能不再被使用,我们需要及时删除,另一方面,有些纹理只是暂时不会被使用,这时候要避免重复上传纹理,应该要把纹理缓存起来,cocos通过TextureCache来管理所有的Texture2D对象。
一、数据结构
class CC_DLL TextureCache : public Ref{protected:// ********************************************************// ***** 以下是多线程异步加载相关数据结构,下面再讲// ********************************************************struct AsyncStruct;std::thread* _loadingThread;std::deque<AsyncStruct*> _asyncStructQueue;std::deque<AsyncStruct*> _requestQueue;std::deque<AsyncStruct*> _responseQueue;std::mutex _requestMutex;std::mutex _responseMutex;std::condition_variable _sleepCondition;bool _needQuit;int _asyncRefCount;// 通过一个unordered_map存储纹理(纹理缓存所在)// key: string类型,表示纹理对应image file的full path// mapped_type: Texture2D对象std::unordered_map<std::string, Texture2D*> _textures;// etc1压缩纹理对应的alpha纹理文件的前缀名,都是统一的。static std::string s_etc1AlphaFileSuffix;};
二、加载纹理(主线程)
1、使用示例
auto textureCache = Director::getInstance()->getTextureCache();auto imageFilePath1 = "......"; // 纹理1,图像文件途径auto imageFilePath2 = "......"; // 纹理2// 加载都在主线程完成,如果图片过大或者数量过多,可能导致卡顿Texture2D* texture1 = textureCache->addImage(imageFilePath1);Texture2D* texture2 = textureCache->addImage(imageFilePath2);
2、实现原理
addImage
// 从路径为path的image file加载纹理,// 支持的文件格式:.png, .bmp, .tiff, .jpeg, .pvr.Texture2D * TextureCache::addImage(const std::string &path) {Texture2D* texture = nullptr;Image* image = nullptr; // Image的作用就是将image file 加载到内存,比如png、pvr图片文件// 获取完整路径std::string fullpath = FileUtils::getInstance()->fullPathForFilename(path);if (fullpath.size() == 0) {return nullptr;}// 先从缓存中查找auto it = _textures.find(fullpath);if (it != _textures.end())texture = it->second;if (!texture) { // 缓存中没有,才会加载,否则直接返回缓存命中do {image = new (std::nothrow) Image();CC_BREAK_IF(nullptr == image);// (JPG/PNG格式文件需要通过库解压缩获得RGB数据,压缩纹理则直接读文件bool bRet = image->initWithImageFile(fullpath);CC_BREAK_IF(!bRet);texture = new (std::nothrow) Texture2D();// 初始化纹理(glTexImage2D),见纹理知识点if (texture && texture->initWithImage(image)) {#if CC_ENABLE_CACHE_TEXTURE_DATA// cache the texture file nameVolatileTextureMgr::addImageTexture(texture, fullpath);#endif// 插入缓存中_textures.emplace(fullpath, texture);// -- ANDROID ETC1 ALPHA SUPPORTS.// 为ETC1压缩纹理添加alpha支持,见https://www.yuque.com/tvvhealth/cs/hfkcft#yZUOX// 做法就是为每个纹理都额外做一张相同大小alpha纹理,然后在片段着色器中将alpha值与rgb值相乘std::string alphaFullPath = path + s_etc1AlphaFileSuffix; // alpha灰度图是etc文件+后缀"@alpha"if (image->getFileType() == Image::Format::ETC // 针对etc压缩纹理&& !s_etc1AlphaFileSuffix.empty()&& FileUtils::getInstance()->isFileExist(alphaFullPath)) { // alpha灰度图存在。Image alphaImage;if (alphaImage.initWithImageFile(alphaFullPath)) { // alpha纹理Texture2D *pAlphaTexture = new(std::nothrow) Texture2D;if(pAlphaTexture != nullptr && pAlphaTexture->initWithImage(&alphaImage)) {texture->setAlphaTexture(pAlphaTexture);}CC_SAFE_RELEASE(pAlphaTexture);}}//parse 9-patch infothis->parseNinePatchImage(image, texture, path);}else { // 纹理初始化失败,无法加载纹理CCLOG("cocos2d: Couldn't create texture for file:%s in TextureCache", path.c_str());CC_SAFE_RELEASE(texture);texture = nullptr;}} while (0);}CC_SAFE_RELEASE(image);return texture;}// image 已经创建好,key为image file的full pathTexture2D* TextureCache::addImage(Image *image, const std::string &key){CCASSERT(image != nullptr, "TextureCache: image MUST not be nil");CCASSERT(image->getData() != nullptr, "TextureCache: image MUST not be nil");Texture2D * texture = nullptr;do{// 缓冲命中,则直接跳过auto it = _textures.find(key);if (it != _textures.end()) {texture = it->second;break;}// 否则创建Texture2D,texture = new (std::nothrow) Texture2D();if (texture) {if (texture->initWithImage(image)) {_textures.emplace(key, texture);}else {CC_SAFE_RELEASE(texture);texture = nullptr;CCLOG("cocos2d: initWithImage failed!");}}else {CCLOG("cocos2d: Allocating memory for Texture2D failed!");}} while (0);#if CC_ENABLE_CACHE_TEXTURE_DATAVolatileTextureMgr::addImage(texture, image);#endifreturn texture;}
三、异步加载纹理
1、使用示例
auto textureCache = Director::getInstance()->getTextureCache();// 纹理1auto imageFilePath1 = "......"; // 图像文件途径auto finishedCallback1 = [](Texture2D* texture){}; // 纹理1加载完成回调auto finishedCallbackKey1 = "finishedCallbackKey1"; // 用于清除加载完成回调,使得加载完成之后,不会触发回调。// 纹理2auto imageFilePath2 = "......";auto finishedCallback2 = [](Texture2D* texture){};auto finishedCallbackKey2 = "finishedCallbackKey2";textureCache->addImageAsync(imageFilePath1, finishedCallback1, finishedCallbackKey1); // 异步加载纹理1textureCache->addImageAsync(imageFilePath2, finishedCallback2, finishedCallbackKey2); // 异步加载纹理2textureCache->unbindImageAsync(finishedCallbackKey2); // 加载纹理2时,不要触发回调textureCache->unbindAllImageAsync(); // 加载所有纹理都不触发完成回调。
2、实现原理
纹理加载是一个耗时的操作,异步加载可以避免卡顿。一个纹理加载可以分两个过程:
- 任务1
- load image data from disk
- 这是最耗时的操作,所以这一步在子线程中完成。
- 一个接一个顺序完成(互斥锁)
- 任务2
- convert image data to texture
- 通过glTexImage2D命令上传纹理,必须线程安全所以在gl线程(主线程)中完成。
- 逐帧执行,每次执行都执行完等待队列中的所有任务,即一帧内将当前所有image convert to texture,cocos认为convert image to texture is faster than load image from disk。
点击查看【processon】
异步加载纹理的相关数据结构:
class CC_DLL TextureCache : public Ref{protected:struct AsyncStruct; // 加载一个纹理的任务结构std::thread* _loadingThread; // load image data from disk的子线程std::deque<AsyncStruct*> _asyncStructQueue; //std::deque<AsyncStruct*> _requestQueue; // 等待需要被加载的std::deque<AsyncStruct*> _responseQueue; // 等待需要被响应的std::mutex _requestMutex;std::mutex _responseMutex;std::condition_variable _sleepCondition;bool _needQuit;int _asyncRefCount;};struct TextureCache::AsyncStruct{public:AsyncStruct(const std::string& fn,const std::function<void(Texture2D*)>& f,const std::string& key ): filename(fn), callback(f), callbackKey( key ), pixelFormat(Texture2D::getDefaultAlphaPixelFormat()), loadSuccess(false){}std::string filename;std::function<void(Texture2D*)> callback;std::string callbackKey;Image image;Image imageAlpha;Texture2D::PixelFormat pixelFormat;bool loadSuccess;};
addImageAsync
相关数据结构:
// 子线程中要执行的一个个纹理加载任务(load image from disk)struct TextureCache::AsyncStruct{public:AsyncStruct(const std::string& fn,const std::function<void( Texture2D * )>& f,const std::string& key ): filename( fn ), callback( f ), callbackKey( key ), pixelFormat( Texture2D::getDefaultAlphaPixelFormat() ), loadSuccess( false ){}std::string filename; // image file full pathstd::function<void( Texture2D * )> callback; // 加载结束的回调std::string callbackKey; // 用于清除callback,使回调不会发生Image image; // image dataImage imageAlpha; // etc1的alpha纹理Texture2D::PixelFormat pixelFormat; // 将纹理加载为指定的格式bool loadSuccess; // 是否加载成功};class CC_DLL TextureCache : public Ref{protected:struct AsyncStruct;std::thread* _loadingThread; // 加载子线程// 双向队列std::deque<AsyncStruct*> _asyncStructQueue; //std::deque<AsyncStruct*> _requestQueue; // 子线程加载任务的等待队列std::deque<AsyncStruct*> _responseQueue; //std::mutex _requestMutex; // 互斥锁std::mutex _responseMutex;std::condition_variable _sleepCondition;bool _needQuit;int _asyncRefCount;};
// 支持格式:.png, .jpgvoid TextureCache::addImageAsync(const std::string &path,const std::function<void(Texture2D*)>& callback){addImageAsync( path, callback, path ); // callback key为image file path}//path : 纹理文件路径,可以是相对路径//callback : 加载完成后的回调,如果加载失败,指针参数nullptr//callbackkey: 标识callback,用于清除加载完成时的回调void TextureCache::addImageAsync(const std::string& path,const std::function<void(Texture2D*)>& callback,const std::string& callbackKey) {Texture2D *texture = nullptr;std::string fullpath = FileUtils::getInstance()->fullPathForFilename(path);//检查纹理是否已经加载,若已加载则直接回调。auto it = _textures.find(fullpath);if (it != _textures.end())texture = it->second;if (texture != nullptr) {if (callback) callback(texture);return;}//文件不存在,直接回调失败if (fullpath.empty() || !FileUtils::getInstance()->isFileExist(fullpath)) {if (callback) callback(nullptr);return;}// 启动线程,用于load image data from diskif (_loadingThread == nullptr) {// create a new thread to load images_needQuit = false;_loadingThread = new (std::nothrow) std::thread(&TextureCache::loadImage, this);}if (0 == _asyncRefCount) {// 逐帧加载(load image data from disk)Director::getInstance()->getScheduler()->schedule(CC_SCHEDULE_SELECTOR( TextureCache::addImageAsyncCallBack), this, 0, false);}++_asyncRefCount;AsyncStruct *data = new (std::nothrow) AsyncStruct(fullpath, callback, callbackKey);_asyncStructQueue.push_back(data);std::unique_lock<std::mutex> ul(_requestMutex);_requestQueue.push_back(data); // 任务入队列_sleepCondition.notify_one();}
loadImage(任务1)
子线程执行,执行load image data from disk任务。
void TextureCache::loadImage() {AsyncStruct *asyncStruct = nullptr;while( !_needQuit ) {std::unique_lock<std::mutex> ul( _requestMutex );// pop an AsyncStruct from request queueif( _requestQueue.empty() ) {asyncStruct = nullptr;}else {asyncStruct = _requestQueue.front();_requestQueue.pop_front();}if( nullptr == asyncStruct ) {if( _needQuit ) {break;}_sleepCondition.wait( ul );continue;}ul.unlock();// load imageasyncStruct->loadSuccess = asyncStruct->image.initWithImageFileThreadSafe( asyncStruct->filename );// ETC1 ALPHA supports.// check whether alpha texture exists & load itif( asyncStruct->loadSuccess &&asyncStruct->image.getFileType() == Image::Format::ETC &&!s_etc1AlphaFileSuffix.empty() ) {auto alphaFile = asyncStruct->filename + s_etc1AlphaFileSuffix;if( FileUtils::getInstance()->isFileExist( alphaFile ) ){ asyncStruct->imageAlpha.initWithImageFileThreadSafe( alphaFile ); }}// 任务1完成,转入_responseQueue队列,等待执行任务2// push the asyncStruct to response queue_responseMutex.lock();_responseQueue.push_back( asyncStruct );_responseMutex.unlock();}}
addImageAsyncCallBack(任务2)
逐帧执行,每次都清空_responseQueue。
void TextureCache::addImageAsyncCallBack( float /*dt*/ ){Texture2D *texture = nullptr;AsyncStruct *asyncStruct = nullptr;while( true ){// pop an AsyncStruct from response queue_responseMutex.lock();if( _responseQueue.empty() ) {asyncStruct = nullptr;}else {asyncStruct = _responseQueue.front();_responseQueue.pop_front();// the asyncStruct's sequence order in _asyncStructQueue must equal to the order in _responseQueueCC_ASSERT( asyncStruct == _asyncStructQueue.front() );_asyncStructQueue.pop_front();}_responseMutex.unlock();if( nullptr == asyncStruct ){break;}// check the image has been convert to texture or notauto it = _textures.find( asyncStruct->filename );if( it != _textures.end() ){texture = it->second;}else{// convert image to textureif( asyncStruct->loadSuccess ){Image *image = &( asyncStruct->image );// generate texture in render threadtexture = new( std::nothrow ) Texture2D();texture->initWithImage( image, asyncStruct->pixelFormat );//parse 9-patch infothis->parseNinePatchImage( image, texture, asyncStruct->filename );#if CC_ENABLE_CACHE_TEXTURE_DATA// cache the texture file nameVolatileTextureMgr::addImageTexture( texture, asyncStruct->filename );#endif// cache the texture. retain it, since it is added in the map_textures.emplace( asyncStruct->filename, texture );texture->retain();texture->autorelease();// ETC1 ALPHA supports.if( asyncStruct->imageAlpha.getFileType() == Image::Format::ETC ){auto alphaTexture = new( std::nothrow ) Texture2D();if( alphaTexture != nullptr && alphaTexture->initWithImage( &asyncStruct->imageAlpha, asyncStruct->pixelFormat ) ){texture->setAlphaTexture( alphaTexture );}CC_SAFE_RELEASE( alphaTexture );}}else{texture = nullptr;CCLOG( "cocos2d: failed to call TextureCache::addImageAsync(%s)", asyncStruct->filename.c_str() );}}// call callback functionif( asyncStruct->callback ){( asyncStruct->callback )( texture );}// release the asyncStructdelete asyncStruct;--_asyncRefCount;}if( 0 == _asyncRefCount ){Director::getInstance()->getScheduler()->unschedule( CC_SCHEDULE_SELECTOR( TextureCache::addImageAsyncCallBack ), this );}}
四、删除纹理
class CC_DLL TextureCache : public Ref {Texture2D* getTextureForKey(const std::string& key) const; // 根据key返回缓存中的纹理,key可以是相对或者绝对路径void removeTextureForKey(const std::string &key); // 引用计数-1// 移除缓存中的所有缓存纹理,引用计数全部-1// Call this method if you receive the "Memory Warning".void removeAllTextures();// 释放引用计数=1的缓存纹理内存// 纹理Texture2D被创建时,引用计数=1,因此=1表示此纹理处于空闲未被使用状态。注意空闲不代表后面不会被使用。// 注意!!!这个函数真正释放了纹理内存,其他的remove是引用计数-1,并不一定会马上释放内存,比如该纹理正在被场景使用// 那么当场景移除该纹理关联的Sprite时,纹理才会被真正释放。void removeUnusedTextures();void removeTexture(Texture2D* texture); // 移除缓存中的指定纹理,引用计数-1}
五、缓存统计
getDescription
输出当前缓存纹理数量情况。
std::string TextureCache::getDescription() const{return StringUtils::format( "<TextureCache | Number of textures = %d>", static_cast<int>( _textures.size() ) );}
getCachedTextureInfo
统计每个缓存纹理的以下信息:
- 引用计数
- texture id
- 像素宽高
- bpp(像素大小,bits per pixel)
- 字节数 ```cpp
std::string TextureCache::getCachedTextureInfo() const { std::string buffer; char buftmp[4096];
unsigned int count = 0;unsigned int totalBytes = 0;for( auto &texture : _textures ){memset( buftmp, 0, sizeof( buftmp ) );Texture2D *tex = texture.second;unsigned int bpp = tex->getBitsPerPixelForFormat();// Each texture takes up width * height * bytesPerPixel bytes.auto bytes = tex->getPixelsWide() * tex->getPixelsHigh() * bpp / 8;totalBytes += bytes;count++;snprintf( buftmp, sizeof( buftmp ) - 1, "\"%s\" rc=%lu id=%lu %lu x %lu @ %ld bpp => %lu KB\n",texture.first.c_str(),( long ) tex->getReferenceCount(),( long ) tex->getName(),( long ) tex->getPixelsWide(),( long ) tex->getPixelsHigh(),( long ) bpp,( long ) bytes / 1024 );buffer += buftmp;}snprintf( buftmp, sizeof( buftmp ) - 1, "TextureCache dumpDebugInfo: %ld textures, for %lu KB (%.2f MB)\n", ( long ) count, ( long ) totalBytes / 1024, totalBytes / ( 1024.0f * 1024.0f ) );buffer += buftmp;return buffer;
}
<a name="4nzON"></a># 六、纹理恢复(Android)在android中,应用程序由后台切换到前台,OpenGL ES上下文可能会被重新创建,这时应用程序中驻留在GPU内存的纹理都将被清空,cocos通过在纹理被创建并成功上传至GPU内存时,保留创建纹理需要的数据(我们叫它纹理信息),然后在适合的时候(调用reloadAllTextures),创建所有纹理并上传至GPU内存,这些工作由VolatileTextureMgr完成。```cpp// ******************* CCTextureCache.h#if CC_ENABLE_CACHE_TEXTURE_DATAclass VolatileTexture { // 缓存纹理的创建数据(创建该纹理需要的数据)typedef enum {kInvalid = 0,kImageFile, // 纹理是通过image file创建,缓存以下数据:// _fileName:纹理文件路径// _pixelFormat:需要转换成的纹理格式kImageData, // 纹理是通过data创建,缓存以下数据:// _textureData: 纹理数据// _dataLen: 纹理数据长度// _pixelFormat: 需要转换成的纹理格式// _textureSize: 纹理像素宽高kString, // 字体纹理,缓存以下数据:// _text字符// _fontDefinitionkImage, // 纹理是通过CCImage创建,缓存以下数据:// _uiImage:CCImage数据} ccCachedImageType;private:VolatileTexture(Texture2D *t);~VolatileTexture();protected:ccCachedImageType _cashedImageType; // 纹理类型。// 以下参数,根据对应的ccCachedImageType来使用friend class VolatileTextureMgr;Texture2D *_texture;Image *_uiImage;void *_textureData;int _dataLen;Size _textureSize;Texture2D::PixelFormat _pixelFormat;std::string _fileName;bool _hasMipmaps;Texture2D::TexParams _texParams; // 过滤(Filtering)和环绕(Wraping)模式std::string _text;FontDefinition _fontDefinition;};class CC_DLL VolatileTextureMgr {public:// 缓存通过file创建的Texture2Dstatic void addImageTexture(Texture2D *tt, const std::string& imageFileName);// 缓存字体纹理static void addStringTexture(Texture2D *tt, const char* text, const FontDefinition& fontDefinition);// 缓存通过data创建的纹理static void addDataTexture(Texture2D *tt, void* data, int dataLen, Texture2D::PixelFormat pixelFormat, const Size& contentSize);// 缓存通过Image创建的纹理static void addImage(Texture2D *tt, Image *image);// 是否是glGenerateMimap动态生成的多级纹理,通过外部工具生成的纹理其实就是当成普通纹理来处理。static void setHasMipmaps(Texture2D *t, bool hasMipmaps);// 缓存纹理参数static void setTexParameters(Texture2D *t, const Texture2D::TexParams &texParams);// 清除纹理对应的缓存数据,释放对应的VolatileTexture内存static void removeTexture(Texture2D *t);// 重新加载所有缓存纹理,在合适的时机调用,在重新加载之前会先清除一次GPU中相关纹理的缓存。static void reloadAllTextures();public:static std::list<VolatileTexture*> _textures; // 双向链表,相关知识:https://www.yuque.com/tvvhealth/cs/cxu0kmstatic bool _isReloading; // 重新加载纹理是一个耗时的操作,添加状态识别。private:// find VolatileTexture by Texture2D*// if not found, create a new onestatic VolatileTexture* findVolotileTexture(Texture2D *tt);static void reloadTexture(Texture2D* texture, const std::string& filename, Texture2D::PixelFormat pixelFormat);};#endif
