cocos2d-x是如何异步的去加载图片

发表于2015-12-26
评论0 2k浏览

使用cocos2d-x的人应该都知道CCTextureCache类中有个异步加载图片的方法addImageAsync,也都知道其背后的实现原理肯定是基于多线程实现的,那么其背后的具体实现逻辑具体是怎样的,所以就带着好奇心的去把它研究了下:

先看addImageAsync的实现:

  1. void CCTextureCache::addImageAsync(const char *path, CCObject *target, SEL_CallFuncO selector)  
  2. {  
  3. #ifdef EMSCRIPTEN//在2.2.2的版本中,不能通过脚本语言(lua和js)异步加载图片(听说qiuck-cocos2d-x实现了用脚本来异步加载图片)  
  4.     CCLOGWARN("Cannot load image %s asynchronously in Emscripten builds.", path);  
  5.     return;  
  6. #endif // EMSCRIPTEN  
  7.   
  8.     CCAssert(path != NULL, "TextureCache: fileimage MUST not be NULL");//要加载的图片的路径不能为空  
  9.   
  10.     CCTexture2D *texture = NULL;  
  11.   
  12.     // optimization  
  13.   
  14.     std::string pathKey = path;//创建字符串pathKey做为字典查询关键字。  
  15.   
  16.     pathKey = CCFileUtils::sharedFileUtils()->fullPathForFilename(pathKey.c_str());//取得图片所在位置的全路径名 ,充当字典中的key  
  17.   
  18.     texture = (CCTexture2D*)m_pTextures->objectForKey(pathKey.c_str());//先查询一下是否字典里已经有了此纹理。  
  19.   
  20.     std::string fullpath = pathKey;//图片所在位置的全路径名  
  21.   
  22.   
  23.     if (texture != NULL)//如果字典存在该纹理图片,则不需要异步加载,直接执行回调函数  
  24.     {  
  25.         if (target && selector)  
  26.         {  
  27.             (target->*selector)(texture);//执行回调,将纹理作为参数传入  
  28.         }  
  29.           
  30.         return;  
  31.     }  
  32.   
  33.   
  34.     // lazy init  
  35.     if (s_pAsyncStructQueue == NULL)//如果是第一次调用多线程载入,创建信号量并进行相应初始化。  
  36.     {               
  37.         s_pAsyncStructQueue = new queue<AsyncStruct*>();//创建一个存储异步信息队列  
  38.         s_pImageQueue = new queue<ImageInfo*>();        //创建一个存储图片信息队列  
  39.         //线程锁初始化  
  40.         pthread_mutex_init(&s_asyncStructQueueMutex, NULL);  
  41.         pthread_mutex_init(&s_ImageInfoMutex, NULL);  
  42.         pthread_mutex_init(&s_SleepMutex, NULL);  
  43.         pthread_cond_init(&s_SleepCondition, NULL);  
  44. #if (CC_TARGET_PLATFORM != CC_PLATFORM_WINRT) && (CC_TARGET_PLATFORM != CC_PLATFORM_WP8)  
  45.         pthread_create(&s_loadingThread, NULL, loadImage, NULL); //创建加载线程。  
  46. #endif  
  47.         need_quit = false//将退出指令设为false。  
  48.     }  
  49.   
  50.     if (0 == s_nAsyncRefCount)//多线程加载图片的引用计数器如果为0,  
  51.     {//用定时器处理异步加载图片(注意:其中的第三个参数为0,也就意味着这个定时器每帧都会被调用,也就是每帧都会调用一次addImageAsyncCallBack)  
  52.         CCDirector::sharedDirector()->getScheduler()->scheduleSelector(schedule_selector(CCTextureCache::addImageAsyncCallBack), this, 0, false);  
  53.     }  
  54.   
  55.     ++s_nAsyncRefCount;//计数器加1(这个参数是用来记录有多少个图片是准备用多线程去加载)  
  56.   
  57.     if (target)  
  58.     {  
  59.         target->retain();//保证addImageAsyncCallBack方法中的回调函数能正确执行  
  60.     }  
  61.   
  62.     // generate async struct  
  63.     AsyncStruct *data = new AsyncStruct();// 产生一个新的加载消息,放入加载消息队列中。  
  64.     data->filename = fullpath.c_str();  
  65.     data->target = target;  
  66.     data->selector = selector;  
  67.   
  68. #if (CC_TARGET_PLATFORM != CC_PLATFORM_WINRT) && (CC_TARGET_PLATFORM != CC_PLATFORM_WP8)  
  69.     // add async struct into queue  
  70.     // 在将异步资源放入队列前需要加锁  
  71.     pthread_mutex_lock(&s_asyncStructQueueMutex);  
  72.     s_pAsyncStructQueue->push(data);  
  73.     pthread_mutex_unlock(&s_asyncStructQueueMutex);  
  74.     pthread_cond_signal(&s_SleepCondition);//发送信量号,唤醒异步加载线程  
  75. #else  
  76.     // WinRT uses an Async Task to load the image since the ThreadPool has a limited number of threads  
  77.     //std::replace( data->filename.begin(), data->filename.end(), '/', '\');   
  78.     create_task([this, data] {  
  79.         loadImageData(data);  
  80.     });  
  81. #endif  
  82. }  
这里由于涉及到几个重要的变量,所以在往下研究之前先弄明白这些变量的意思

1)存放异步加载信息的结构体,里面存放了文件名,调用者,和加载完后的回调函数

  1. typedef struct _AsyncStruct  
  2. {  
  3.     std::string            filename;//文件名  
  4.     CCObject    *target;//调用者  
  5.     SEL_CallFuncO        selector;//加载完的回调函数  
  6. } AsyncStruct;  
2)存放图片信息的结构体(里面存放了异步加载所需的结构体,图片指针,图片类型)

  1. typedef struct _ImageInfo//图片信息  
  2. {  
  3.     AsyncStruct *asyncStruct;//异步加载结构体  
  4.     CCImage        *image;//图片指针  
  5.     CCImage::EImageFormat imageType;//图片类型  
  6. } ImageInfo;  
3)需要用多线程加载的图片的数量:

  1. static unsigned long s_nAsyncRefCount = 0;  

4)与线程相关的四个全局静态变量

  1. static pthread_t s_loadingThread;//加载图片的线程  
  2.   
  3. static pthread_mutex_t      s_SleepMutex;//用于线程睡眠的线程临界区  
  4. static pthread_cond_t       s_SleepCondition;//用于唤醒在睡眠中的线程的条件  
  5.   
  6. static pthread_mutex_t      s_asyncStructQueueMutex;//用于读取异步加载信息队列的线程临界区  
  7. static pthread_mutex_t      s_ImageInfoMutex;//用于处理存储图片信息结构的临界区  

大概理清了上面的变量后就可以往下看addImageAsyncCallBack。当调用了addImageAsync方法后会通过定时器每帧的去调用addImageAsyncCallBack方法:

  1. void CCTextureCache::addImageAsyncCallBack(float dt)  
  2. {  
  3.     // the image is generated in loading thread  
  4.     std::queue<ImageInfo*> *imagesQueue = s_pImageQueue;// 取得多线程加载的图片信息队列(在线程执行完加载图片后会将图片的信息结构体加入到s_pImageQueue队列中)  
  5.     //下面代码作为临界区上锁  
  6.     pthread_mutex_lock(&s_ImageInfoMutex);//上s_ImageInfoMutex锁  
  7.     if (imagesQueue->empty())//如果图片信息队列为空直接解锁,否则进行处理  
  8.     {  
  9.         pthread_mutex_unlock(&s_ImageInfoMutex);  
  10.     }  
  11.     else  
  12.     {  
  13.         ImageInfo *pImageInfo = imagesQueue->front();//取出图片信息队列的头一个信息从队列中弹出。  
  14.         imagesQueue->pop();//取出后从头部弹出来,也就是说先加载的图片先处理  
  15.         pthread_mutex_unlock(&s_ImageInfoMutex);//解锁临界区  
  16.   
  17.         AsyncStruct *pAsyncStruct = pImageInfo->asyncStruct;//取得信息中的加载消息。  
  18.         CCImage *pImage = pImageInfo->image;//取得图片信息中的CCImage指针。  
  19.   
  20.         CCObject *target = pAsyncStruct->target; //取得加载完成后要通知的对象以及要调用的函数。  
  21.         SEL_CallFuncO selector = pAsyncStruct->selector;//回调函数指针  
  22.         const char* filename = pAsyncStruct->filename.c_str(); //取得图片文件名  
  23.   
  24.         // generate texture in render thread  
  25.         CCTexture2D *texture = new CCTexture2D();// 新建一个纹理。  
  26.         //使用CCImage指针pImage来初始化纹理生成OpenGL贴图。  
  27. #if 0 //TODO: (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)  
  28.         texture->initWithImage(pImage, kCCResolutioniPhone);  
  29. #else  
  30.         texture->initWithImage(pImage);  
  31. #endif  
  32.   
  33. #if CC_ENABLE_CACHE_TEXTURE_DATA  
  34.        // cache the texture file name  
  35.        VolatileTexture::addImageTexture(texture, filename, pImageInfo->imageType);  
  36. #endif  
  37.   
  38.         // cache the texture  
  39.         m_pTextures->setObject(texture, filename);//使用文件名做为查询关键字将纹理存入字典  
  40.         //这里将texture的引用计数通过内存回收池减1,最后texture的引用计数还原成1,因为上面的setObject会将texture的引用计数加1(其实这里直接调用release更好)  
  41.         //(这里一定要将引用计数置为1,因为removeUnusedTextures方法与它有关,removeUnusedTextures是一个回收无用贴图的意思,调用它会将引用计数为1的纹理全部release掉,这就是这里要保持为1的原因)  
  42.         texture->autorelease();  
  43.   
  44.         if (target && selector)//调用通知目标的相应函数。  
  45.         {  
  46.             (target->*selector)(texture);  
  47.             target->release();//在调用addImageAsync方法时会将target retain,所以这里对应的要release下  
  48.         }          
  49.   
  50.         pImage->release();//加载完毕释放CCImage对象。  
  51.         //释放new出来的消息结构和图片信息结构。  
  52.         delete pAsyncStruct;  
  53.         delete pImageInfo;  
  54.   
  55.         --s_nAsyncRefCount;//多线程加载图片的数量减1,  
  56.         if (0 == s_nAsyncRefCount)  
  57.         {//当所有的需要异步加载的图片全部加载完后,从全局定时器中移除掉用于加载的图片的定时器,因为这时没有图片需要异步加载,所以定时器已经没用了  
  58.             CCDirector::sharedDirector()->getScheduler()->unscheduleSelector(schedule_selector(CCTextureCache::addImageAsyncCallBack), this);  
  59.         }  
  60.     }  
  61. }  
可以看出在每帧调用addImageAsyncCallBack方法时的第一个条件便是s_pImageQueue队列要有内容,要使这个队列有内容就必须要通过线程去调用loadImage方法,这个方法会将加载完的图片信息保存到这个队列中(在初始化线程时已经启动了该线程(addImageAsync方法中):pthread_create(&s_loadingThread, NULL, loadImage, NULL))。

loadImage方法:

  1. static void* loadImage(void* data)  
  2. {  
  3.     AsyncStruct *pAsyncStruct = NULL;  
  4.   
  5.     while (true)//死循环  
  6.     {  
  7.         // create autorelease pool for iOS  
  8.         CCThread thread;// 创建一个线程信息对象(这里这个对象是没有用到,估计是以后的版本会用,整个CCThread类也都是空实现)  
  9.         thread.createAutoreleasePool();//空实现  
  10.   
  11.         std::queue<AsyncStruct*> *pQueue = s_pAsyncStructQueue;  
  12.         pthread_mutex_lock(&s_asyncStructQueueMutex);//在处理异步加载信息的时候上锁  
  13.         if (pQueue->empty())//如果是空队列,解锁  
  14.         {  
  15.             pthread_mutex_unlock(&s_asyncStructQueueMutex);//解锁  
  16.             if (need_quit) {//是否退出(只有调用析构函数时这个变量才会被置为true,然后销毁线程)  
  17.                 break;  
  18.             }  
  19.             else {//如果没有退出循环,则等待addImageAsync方法调用时发送信号量,唤醒线程,节省cpu资源  
  20.                 pthread_cond_wait(&s_SleepCondition, &s_SleepMutex);  
  21.                 continue;  
  22.             }  
  23.         }  
  24.         else  
  25.         {  
  26.             pAsyncStruct = pQueue->front();//从异步加载信息的结构体队列中取出第一个要处理的结构体  
  27.             pQueue->pop();//第一个结构体从队列中出列  
  28.             pthread_mutex_unlock(&s_asyncStructQueueMutex);//解锁  
  29.             loadImageData(pAsyncStruct);//开始加载图片  
  30.         }          
  31.     }  
  32.       
  33.     if( s_pAsyncStructQueue != NULL )//当线程退出时,即被析构时清空资源  
  34.     {  
  35.         delete s_pAsyncStructQueue;  
  36.         s_pAsyncStructQueue = NULL;  
  37.         delete s_pImageQueue;  
  38.         s_pImageQueue = NULL;  
  39.   
  40.         pthread_mutex_destroy(&s_asyncStructQueueMutex);  
  41.         pthread_mutex_destroy(&s_ImageInfoMutex);  
  42.         pthread_mutex_destroy(&s_SleepMutex);  
  43.         pthread_cond_destroy(&s_SleepCondition);  
  44.     }  
  45.       
  46.     return 0;  
  47. }  
最后便是加载图片信息的方法:

  1. static void loadImageData(AsyncStruct *pAsyncStruct)  
  2. {  
  3.     const char *filename = pAsyncStruct->filename.c_str();//文件名  
  4.   
  5.     // compute image type  
  6.     CCImage::EImageFormat imageType = computeImageFormatType(pAsyncStruct->filename);//文件格式  
  7.     if (imageType == CCImage::kFmtUnKnown)//不支持的格式则返回并打印信息  
  8.     {  
  9.         CCLOG("unsupported format %s",filename);  
  10.         delete pAsyncStruct;  
  11.         return;  
  12.     }  
  13.           
  14.     // generate image              
  15.     CCImage *pImage = new CCImage();//创建一个图片  
  16.     if (pImage && !pImage->initWithImageFileThreadSafe(filename, imageType))//读取是否成功  
  17.     {//读取失败,打印信息  
  18.         CC_SAFE_RELEASE(pImage);  
  19.         CCLOG("can not load %s", filename);  
  20.         return;  
  21.     }  
  22.   
  23.     // generate image info  
  24.     ImageInfo *pImageInfo = new ImageInfo();//图片信息  
  25.     pImageInfo->asyncStruct = pAsyncStruct;//异步结构信息(文件名,调用者,回调函数)  
  26.     pImageInfo->image = pImage;//图片  
  27.     pImageInfo->imageType = imageType;//图片类型  
  28.     // put the image info into the queue  
  29.     pthread_mutex_lock(&s_ImageInfoMutex);//上锁,防止addImageAsyncCallBack方法破坏队列  
  30.     s_pImageQueue->push(pImageInfo);//将图片信息放入队列  
  31.     pthread_mutex_unlock(&s_ImageInfoMutex);//解锁  
  32. }  

总结:

1)cocos2d-x异步加载图片时其实只启动了一个线程通过维护队列的方式去加载,而不是没加载一次图片,启动一个新线程

2)在处理两个队列s_pImageQueue和s_pAsyncStructQueue时都需要互斥,这样才能保证队列的出队和入队能有序的被处理

3)当没有图片需要异步加载时,线程要进行休眠,知道有新的图片需要异步加载,然后通过发送信号量去唤醒它


如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引