cocos2d-x TextureCache::addImageAsync 方法的陷阱

转载自 http://www.58player.com/blog-2479-108013.html

最近遇到一个问题就是使用TextureCache::addImageAsync异步加载图片资源的时候会出现无限加载,图片不能正常加载完成导致无法接收到回调事件,于是网上查了一下发现了这个方法潜在的问题,以下内容转载自superman的博客,感谢他的分析让我查到了问题所在。

就以往的经验,异步加载图片是一个复杂的工作,往往容易出现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的参数已经是绝对路径,所以没有上述的问题。

总结
这陷阱的数量和浅显程度,让我有一种想要重新写一套异步加载机制的冲动了。然则,暂时还是以修改为主,以重写为辅吧。上述的那些内容都修改掉,多多测试,看看是否有其它问题。
总之,cocos2d-x虽然流行,但是稳定性和质量真的还很值得思量。使用时需要多多小心。

补充


今天又出现加载不了资源的情况,然后又打开源代码仔细查看了一下,发现代码中可能会出现线程无限等待的情况。

if (_asyncStructQueue == nullptr)
    {             
        _asyncStructQueue = new queue<AsyncStruct*>();
        _imageInfoQueue   = new deque<ImageInfo*>();

        _loadingThread = new std::thread(&TextureCache::loadImage, this);
        _needQuit = false;
    }

    if (0 == _asyncRefCount)
    {
        Director::getInstance()->getScheduler()->schedule(CC_SCHEDULE_SELECTOR(TextureCache::addImageAsyncCallBack), this, 0, false);
    }

    ++_asyncRefCount;

    // generate async struct
    AsyncStruct *data = new (std::nothrow) AsyncStruct(fullpath, callback);

    // add async struct into queue
    _asyncStructQueueMutex.lock();
    _asyncStructQueue->push(data);
    _asyncStructQueueMutex.unlock();

    _sleepCondition.notify_one();

在这段代码中,能够发现是先创建了_loadingThread线程,然后在代码末尾将AsyncStruct *data对象添加到_asyncStructQueue中去,最后通过调用_sleepCondition.notify_one()唤醒等待的线程。
下面让我们看看_loadingThread线程是怎么实现。

void TextureCache::loadImage()
{
    AsyncStruct *asyncStruct = nullptr;

    while (true)
    {
        std::queue<AsyncStruct*> *pQueue = _asyncStructQueue;
        _asyncStructQueueMutex.lock();
        if (pQueue->empty())
        {
            _asyncStructQueueMutex.unlock();
            if (_needQuit) {
                break;
            }
            else {
                std::unique_lock<std::mutex> lk(_sleepMutex);
                _sleepCondition.wait(lk);
                continue;
            }
        }
        else
        {
            asyncStruct = pQueue->front();
            pQueue->pop();
            _asyncStructQueueMutex.unlock();
        }        

        Image *image = nullptr;
        bool generateImage = false;

        auto it = _textures.find(asyncStruct->filename);
        if( it == _textures.end() )
        {
           _imageInfoMutex.lock();
           ImageInfo *imageInfo;
           size_t pos = 0;
           size_t infoSize = _imageInfoQueue->size();
           for (; pos < infoSize; pos++)
           {
               imageInfo = (*_imageInfoQueue)[pos];
               if(imageInfo->asyncStruct->filename.compare(asyncStruct->filename) == 0)
                   break;
           }
           _imageInfoMutex.unlock();
           if(infoSize == 0 || pos == infoSize)
               generateImage = true;
        }

        if (generateImage)
        {
            const std::string& filename = asyncStruct->filename;
            // generate image      
            image = new (std::nothrow) Image();
            if (image && !image->initWithImageFileThreadSafe(filename))
            {
                CC_SAFE_RELEASE(image);
                CCLOG("can not load %s", filename.c_str());
                continue;
            }
        }    

        // generate image info
        ImageInfo *imageInfo = new (std::nothrow) ImageInfo();
        imageInfo->asyncStruct = asyncStruct;
        imageInfo->image = image;

        // put the image info into the queue
        _imageInfoMutex.lock();
        _imageInfoQueue->push_back(imageInfo);
        _imageInfoMutex.unlock();
    }

    if(_asyncStructQueue != nullptr)
    {
        delete _asyncStructQueue;
        _asyncStructQueue = nullptr;
        delete _imageInfoQueue;
        _imageInfoQueue = nullptr;
    }

TextureCache::loadImage()方法是通过一个while循环去处理图片加载的,当判断_asyncStructQueue队列里面有对象,就去加载图片,否则就_sleepCondition.wait(lk),等待与之对象的_sleepCondition.notify_one()唤醒它,这时候问题就出现了,这两个方法是在两个线程中执行的,当线程第一次创建的时候,假如主线程的方法已经执行_sleepCondition.notify_one(),而资源加载的线程又刚好在pQueue->empty()等情况下继续往下执行,但是还没有执行到_sleepCondition.wait(lk)函数,这时候_sleepCondition.notify_one()函数是没有作用的,资源加载线程再执行到_sleepCondition.wait(lk)函数的时候,线程就进入了等待中,这时候就无法加载当前资源了。
我修改的方法是如下

// lazy init
    if (_asyncStructQueue == nullptr)
    {             
        _asyncStructQueue = new queue<AsyncStruct*>();
        _imageInfoQueue   = new deque<ImageInfo*>();
      //先不创建线程,最后创建
//        _loadingThread = new std::thread(&TextureCache::loadImage, this);
//        _needQuit = false;
    }

创建线程的地方

    AsyncStruct *data = new (std::nothrow) AsyncStruct(fullpath, callback);

    // add async struct into queue
    _asyncStructQueueMutex.lock();
    _asyncStructQueue->push(data);
    _asyncStructQueueMutex.unlock();

    _sleepCondition.notify_one();

    //这里创建线程
    if(!_loadingThread){
        _needQuit = false;
        _loadingThread = new std::thread(&TextureCache::loadImage, this);
    }

这样就保证了线程创建完成之后,_asyncStructQueue这时候是有对象的,第一次就不会进入到pQueue->empty()的情况,以后加载都能正常运行了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值