在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 name
VolatileTextureMgr::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 info
this->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 path
Texture2D* 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_DATA
VolatileTextureMgr::addImage(texture, image);
#endif
return texture;
}
三、异步加载纹理
1、使用示例
auto textureCache = Director::getInstance()->getTextureCache();
// 纹理1
auto imageFilePath1 = "......"; // 图像文件途径
auto finishedCallback1 = [](Texture2D* texture){}; // 纹理1加载完成回调
auto finishedCallbackKey1 = "finishedCallbackKey1"; // 用于清除加载完成回调,使得加载完成之后,不会触发回调。
// 纹理2
auto imageFilePath2 = "......";
auto finishedCallback2 = [](Texture2D* texture){};
auto finishedCallbackKey2 = "finishedCallbackKey2";
textureCache->addImageAsync(imageFilePath1, finishedCallback1, finishedCallbackKey1); // 异步加载纹理1
textureCache->addImageAsync(imageFilePath2, finishedCallback2, finishedCallbackKey2); // 异步加载纹理2
textureCache->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 path
std::function<void( Texture2D * )> callback; // 加载结束的回调
std::string callbackKey; // 用于清除callback,使回调不会发生
Image image; // image data
Image 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, .jpg
void 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 disk
if (_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 queue
if( _requestQueue.empty() ) {
asyncStruct = nullptr;
}
else {
asyncStruct = _requestQueue.front();
_requestQueue.pop_front();
}
if( nullptr == asyncStruct ) {
if( _needQuit ) {
break;
}
_sleepCondition.wait( ul );
continue;
}
ul.unlock();
// load image
asyncStruct->loadSuccess = asyncStruct->image.initWithImageFileThreadSafe( asyncStruct->filename );
// ETC1 ALPHA supports.
// check whether alpha texture exists & load it
if( 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 _responseQueue
CC_ASSERT( asyncStruct == _asyncStructQueue.front() );
_asyncStructQueue.pop_front();
}
_responseMutex.unlock();
if( nullptr == asyncStruct )
{
break;
}
// check the image has been convert to texture or not
auto it = _textures.find( asyncStruct->filename );
if( it != _textures.end() )
{
texture = it->second;
}
else
{
// convert image to texture
if( asyncStruct->loadSuccess )
{
Image *image = &( asyncStruct->image );
// generate texture in render thread
texture = new( std::nothrow ) Texture2D();
texture->initWithImage( image, asyncStruct->pixelFormat );
//parse 9-patch info
this->parseNinePatchImage( image, texture, asyncStruct->filename );
#if CC_ENABLE_CACHE_TEXTURE_DATA
// cache the texture file name
VolatileTextureMgr::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 function
if( asyncStruct->callback )
{
( asyncStruct->callback )( texture );
}
// release the asyncStruct
delete 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_DATA
class VolatileTexture { // 缓存纹理的创建数据(创建该纹理需要的数据)
typedef enum {
kInvalid = 0,
kImageFile, // 纹理是通过image file创建,缓存以下数据:
// _fileName:纹理文件路径
// _pixelFormat:需要转换成的纹理格式
kImageData, // 纹理是通过data创建,缓存以下数据:
// _textureData: 纹理数据
// _dataLen: 纹理数据长度
// _pixelFormat: 需要转换成的纹理格式
// _textureSize: 纹理像素宽高
kString, // 字体纹理,缓存以下数据:
// _text字符
// _fontDefinition
kImage, // 纹理是通过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创建的Texture2D
static 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/cxu0km
static bool _isReloading; // 重新加载纹理是一个耗时的操作,添加状态识别。
private:
// find VolatileTexture by Texture2D*
// if not found, create a new one
static VolatileTexture* findVolotileTexture(Texture2D *tt);
static void reloadTexture(Texture2D* texture, const std::string& filename, Texture2D::PixelFormat pixelFormat);
};
#endif