addImageAsync异步加载
未响应回调前调用unbindImageAsync撤销消息回调
void TextureCache::unbindImageAsync(const std::string& filename)
{
_imageInfoMutex.lock();
if (_imageInfoQueue && !_imageInfoQueue->empty())
{
std::string fullpath = FileUtils::getInstance()->fullPathForFilename(filename);
auto found = std::find_if(_imageInfoQueue->begin(), _imageInfoQueue->end(), [&fullpath](ImageInfo* ptr)->bool{ return ptr->asyncStruct->filename == fullpath; });
if (found != _imageInfoQueue->end())
{
(*found)->asyncStruct->callback = nullptr;
}
}
_imageInfoMutex.unlock();
}
但是在极端情况下,调用 addImageAsync后马上调用unbindImageAsync
此时loadImage线程还未将ImageInfo加入_imageInfoQueue
而是在unbindImageAsync之后添加,导致消息回调解绑失败
无法调用(*found)->asyncStruct->callback = nullptr;
从而在异步加载完成后调用callback时候出现问题
loadImage()线程
同时操作两个链表
_asyncStructQueue
_imageInfoQueue
请求从_asyncStructQueue弹出后生成ImageInfo添加到_imageInfoQueue
void TextureCache::loadImage()
{
_asyncStructQueueMutex.lock();
_asyncStructQueue
_asyncStructQueueMutex.unlock();
.....临界点,执行unbindImageAsync
_imageInfoMutex.lock();
_imageInfoQueue->push_back(imageInfo);
_imageInfoMutex.unlock();
}
此处的Bug是两个链表是分别加锁的
请求从_asyncStructQueue弹出后,未即时插入_imageInfoQueue
在临界点又调用了unbindImageAsync撤销异步加载响应
跟昨天提到的Bug类似,同样会导致撤销失败
在回调时引起异常
=================================================================
以下为转帖
就以往的经验,异步加载图片是一个复杂的工作,往往容易出现bug。
那么,cocos2d-x提供的这个异步加载功能是否可靠?百度了一下,没发现什么重要的信息,于是自己分析之。
照cocos2d-x自身的注释来看,这个addImageAsync函数是从0.8版本就有了,而现在是3.1版本,怎么也该稳定了吧?可惜的是,里面的陷阱并不少。
陷阱1:_textures未加锁
在异步加载时,cocos2d-x主要用了两个队列,即_asyncStructQueue和_imageInfoQueue,在操作这两个队列的时候也都很小心的加锁了。但是对_textures的访问则没有加锁。因此,如果先用addImageAsync进行异步加载图片A,再用addImage同步加载图片B,则有几率导致_textures这个对象被损坏,进而导致程序不稳定。
修改:由于涉及到_textures的地方很多,逐一加锁实在麻烦,所以干脆不加锁,转为让异步线程不要访问_textures。大不了就是同一张图片被多次加载,浪费一些运算量罢了。上层代码小心控制的话,是不会真的有图片被重复加载的。
陷阱2:判断逻辑错误
在cocos2d-x 3.1.1版本中,异步加载的代码中有一句判断:if(imageInfo->asyncStruct->filename.compare(asyncStruct->filename)),这是有问题的。
作者的本意可能是想,如果队列中有多个请求都是加载同一幅图片,那么其实只需要加载一次即可。可惜这个判断写反了,下文又有一处判断写反,导致不知所云。
这个问题在cocos2d-x 3.2版本已经修复了。
P.S. 字符串比较,还是用==、!=这样的操作符比较好,可读性和运行性能都要优于compare函数。
修改:cocos官方已经修正。不过其实跟陷阱1类似,不必判断,大不了就是同一张图片被多次加载,浪费一些运算量罢了。
陷阱3:insert失败导致内存泄漏
在异步加载完毕之后,主线程有一句:_textures.insert( std::make_pair(filename, texture) );。
由于陷阱1、陷阱2中,我们并没有进行彻底的检查,所以有几率出现重复加载的情形。(实际上,除非全程加锁,否则很难彻底避免重复加载。然而,全程加锁的话,异步加载也就没有意义了)。当出现重复加载同一张图片时,这里的insert就会失败。于是,texture不会有被销毁的机会,于是造成内存泄漏。
陷阱4:创建线程时,需要的变量尚未初始化完毕
创建异步加载的线程时,原始代码是先创建线程,再设置_needQuit。
正常应该是先设置_needQuit为false(初始化值为true!),再创建线程。否则理论上有可能线程刚创建完毕就立即结束。
疑似陷阱
异步加载线程和主线程,都调用了Image::initWithImageFileThreadSafe,这个函数看名字似乎是线程安全的,实际上它调用了FileUtils::fullPathForFilename。这个函数除非参数是绝对路径,否则就会对一个名为_fullPathCache的哈希表进行读写,若不加锁就会出错。幸好在异步加载线程中,传入给FileUtils::fullPathForFilename的参数已经是绝对路径,所以没有上述的问题。
http://www.cocoachina.com/bbs/read.php?tid-312395.html
http://www.58player.com/blog-2479-108013.html