cocos2d-x游戏引擎核心之八——多线程

1、多线程原理android

(1)单线程的尴尬编程

  从新回顾下 Cocos2d-x 的并行机制。引擎内部实现了一个庞大的主循环,在每帧之间更新各个精灵的状态、执行动做、调用定时函数等,这些操做之间能够保证严格独立,互不干扰。不得不说,这是一个很是巧妙的机制,它用一个线程就实现了并发,尤为是将连续的动做变化切割为离散的状态更新时,利用帧间间隔刷新这些状态即实现了多个动做的模拟。windows

  但这在本质上毕竟是一个串行的过程,一种尴尬的场景是,咱们须要执行一个大的计算任务,两帧之间几十毫秒的时间根本不可能完成,例如加载几十张图片到内存中,这时候引擎提供的 schedule 并行就显得无力了:一次只能执行一个小时间片,咱们要么将任务进一步细分为一个个更小的任务,要么只能眼睁睁地看着屏幕上的帧率往下掉,由于这个庞大计算消耗了太多时间,阻塞了主循环的正常运行。api

  原本这个问题是难以免的,可是随着移动设备硬件性能的提升,双核甚至四核的机器已经愈来愈广泛了,若是再不经过多线程挖掘硬件潜力就过于浪费了。缓存

(2)pthead安全

  pthread 是一套 POSIX 标准线程库,能够运行在各个平台上,包括 Android、iOS 和 Windows,也是 Cocos2d-x 官方推荐的多线程库。它使用 C 语言开发,提供很是友好也足够简洁的开发接口。一个线程的建立一般是这样的:网络

void* justAnotherTest(void *arg)
{
    LOG_FUNCTION_LIFE;
    //在这里写入新线程将要执行的代码
    return NULL;
}
void testThread()
{
    LOG_FUNCTION_LIFE;
    pthread_t tid;
    pthread_create(&tid, NULL, &justAnotherTest, NULL);
}

  这里咱们在testThread函数中用pthread_create建立了一个线程,新线程的入口为justAnotherTest函数。pthread_create函数的代码以下所示:多线程

PTW32_DLLPORT int PTW32_CDECL pthread_create (pthread_t * tid,//线程的标示   
                            const pthread_attr_t * attr,      //建立线程的参数   
                            void *(*start) (void *),          //入口函数的指针   
                            void *arg);                       //传递给线程的数据

  pthread_create 是建立新线程的方法,它的第一个参数指定一个标识的地址,用于返回建立的线程标识;第二个参数是建立线程的参数,在不须要设置任何参数的状况下,只需传入 NULL 便可;第三个参数则是线程入口函数的指针,被指定为 void*(void*)的形式。函数指针接受的惟一参数来源于调用 pthread_create 函数时所传入的第四个参数,能够用于传递用户数据。并发

(3)线程安全app

  使用线程就不得不提线程安全问题。线程安全问题来源于不一样线程的执行顺序是不可预测的,线程调度都视系统当时的状态而定,尤为是直接或间接的全局共享变量。若是不一样线程间都存在着读写访问,就极可能出现运行结果不可控的问题。

在 Cocos2d-x 中,最大的线程安全隐患是内存管理。引擎明确声明了 retain、release 和 autorelease 三个方法都不是线程安全的。若是在不一样的线程间对同一个对象做内存管理,可能会出现严重的内存泄露或野指针问题。好比说,若是咱们按照下述代码加载图片资源,就极可能出现找不到图片的报错——可能出现这样的状况,当主线程执行到CCSprite::Create建立精灵的时候,上面的线程尚未执行或者没有执行完成图片资源的加载,这时就可能出现找不到图片。

void* loadResources(void *arg)
{
    LOG_FUNCTION_LIFE;
    CCTextureCache::sharedTextureCache()->addImage("fish.png");
    return NULL;
}
void makeAFish()
{
    LOG_FUNCTION_LIFE;
    pthread_t tid;
    pthread_create(&tid, NULL, &loadResources, NULL);
    CCSprite* sp = CCSprite::create("fish.png");
}

  在新的线程中对缓存的调用所产生的一系列内存管理操做更可能致使系统崩溃。

  所以,使用多线程的首要原则是,在新创建的线程中不要使用任何 Cocos2d-x 内建的内存管理,也不要调用任何引擎提供的函数或方法,由于那可能会致使 Cocos2d-x 内存管理错误

  一样,OpenGL 的各个接口函数也不是线程安全的。也就是说,一切和绘图直接相关的操做都应该放在主线程内执行,而不是在新建线程内执行。(见第六点cocos2dx内存管理与多线程问题)

(4)线程间任务安排

  使用并发编程的最直接目的是保证界面流畅,这也是引擎占据主线程的缘由。所以,除了界面相关的代码外,其余操做均可以放入新的线程中执行,主要包括文件读写和网络通讯两类。

  文件读写涉及外部存储操做,这和内存、CPU 都不在一个响应级别上。若是将其放入主线程中,就可能会形成阻塞,尤其严重的是大型图片的载入。对于碎图压缩后的大型纹理和高分辨率的背景图,一次加载可能耗费 0.2 s 以上的时间,若是彻底放在主线程内,会阻塞主线程至关长的时间,致使画面停滞,游戏体验很糟糕。在一些大型的卷轴类游戏中,这类问题尤其明显。考虑到这个问题,Cocos2d-x 为咱们提供了一个异步加载图片的接口,不会阻塞主线程,其内部正是采用了新建线程的办法。

  咱们用游戏中的背景层为例,原来加载背景层的操做是串行的,相关代码以下:

bool BackgroundLayer::init()
{
    LOG_FUNCTION_LIFE;
    bool bRet = false;
    do {
        CC_BREAK_IF(! CCLayer::init());
        CCSize winSize = CCDirector::sharedDirector()->getWinSize();
        CCSprite *bg = CCSprite::create ("background.png");
        CCSize size = bg->getContentSize();
        bg->setPosition(ccp(winSize.width / 2, winSize.height / 2));
        float f = max(winSize.width / size.width, winSize.height / size.height);
        bg->setScale(f);
        this->addChild(bg);
        bRet = true;
    } while (0);
    return bRet;
}

  如今咱们将这一些列串行的过程分离开来,使用引擎提供的异步加载图片接口异步加载图片,相关代码以下:

void BackgroundLayer::doLoadImage(ccTime dt)
{
    CCSize winSize = CCDirector::sharedDirector()->getWinSize();
    CCSprite *bg = CCSprite::create("background.png");
    CCSize size = bg->getContentSize();
    bg->setPosition(ccp(winSize.width / 2, winSize.height / 2));
    float f = max(winSize.width/size.width,winSize.height/size.height);
    bg->setScale(f);
    this->addChild(bg);
}

void BackgroundLayer::loadImageFinish(CCObject* sender)
{
    this->scheduleOnce(schedule_selector(BackgroundLayer::doLoadImage), 2);
}

bool BackgroundLayer::init()
{
    LOG_FUNCTION_LIFE;
    bool bRet = false;
    do {
        CC_BREAK_IF(! CCLayer::init());
        CCTextureCache::sharedTextureCache()->addImageAsync(
        "background.png",
        this,
        callfuncO_selector(BackgroundLayer::loadImageFinish));
        bRet = true;
    } while (0);
    return bRet;
}

  为了增强效果的对比,咱们在图片加载成功后,延时了 2 s,然后才真正加载背景图片到背景层中。读者能够明显看到,2s后游戏中才出现了背景图。尽管引擎已经为咱们提供了异步加载图片缓存的方式,但考虑到对图片资源的加密解密过程是十分耗费计算资源的,咱们仍是有必要单开一个线程执行这一系列操做。另外一个值得使用并发编程的是网络通讯。网络通讯可能比文件读写要慢一个数量级。通常的网络通讯库都会提供异步传输形式,咱们只须要注意选择就好。

(5)线程同步

使用了线程,必然就要考虑到线程同步,不一样的线程同时访问资源的话,访问的顺序是不可预知的,会形成不可预知的结果。查看addImageAsync的实现源码能够知道它是使用pthread_mutex_t来实现同步:

void CCTextureCache::addImageAsync(const char *path, CCObject *target, SEL_CallFuncO selector)
{
    CCAssert(path != NULL, "TextureCache: fileimage MUST not be NULL");    

    CCTexture2D *texture = NULL;

    // optimization

    std::string pathKey = path;

    pathKey = CCFileUtils::sharedFileUtils()->fullPathFromRelativePath(pathKey.c_str());
    texture = (CCTexture2D*)m_pTextures->objectForKey(pathKey.c_str());

    std::string fullpath = pathKey;
    if (texture != NULL)
    {
        if (target && selector)
        {
            (target->*selector)(texture);
        }
        
        return;
    }

    // lazy init
    if (s_pSem == NULL)
    {             
#if CC_ASYNC_TEXTURE_CACHE_USE_NAMED_SEMAPHORE
        s_pSem = sem_open(CC_ASYNC_TEXTURE_CACHE_SEMAPHORE, O_CREAT, 0644, 0);
        if( s_pSem == SEM_FAILED )
        {
            CCLOG( "CCTextureCache async thread semaphore init error: %s\n", strerror( errno ) );
            s_pSem = NULL;
            return;
        }
#else
        int semInitRet = sem_init(&s_sem, 0, 0);
        if( semInitRet < 0 )
        {
            CCLOG( "CCTextureCache async thread semaphore init error: %s\n", strerror( errno ) );
            return;
        }
        s_pSem = &s_sem;
#endif
        s_pAsyncStructQueue = new queue<AsyncStruct*>();
        s_pImageQueue = new queue<ImageInfo*>();        
        
        pthread_mutex_init(&s_asyncStructQueueMutex, NULL);
        pthread_mutex_init(&s_ImageInfoMutex, NULL);
        pthread_create(&s_loadingThread, NULL, loadImage, NULL);

        need_quit = false;
    }

    if (0 == s_nAsyncRefCount)
    {
        CCDirector::sharedDirector()->getScheduler()->scheduleSelector(schedule_selector(CCTextureCache::addImageAsyncCallBack), this, 0, false);
    }

    ++s_nAsyncRefCount;

    if (target)
    {
        target->retain();
    }

    // generate async struct
    AsyncStruct *data = new AsyncStruct();
    data->filename = fullpath.c_str();
    data->target = target;
    data->selector = selector;

    // add async struct into queue
    pthread_mutex_lock(&s_asyncStructQueueMutex);
    s_pAsyncStructQueue->push(data);
    pthread_mutex_unlock(&s_asyncStructQueueMutex);

    sem_post(s_pSem);
}

 

2、应用实例一——cococs2d-x 多线程加载plist

【转自】 http://blog.csdn.net/we000636/article/details/8641270

(1)环境搭建

当咱们想在程序中开多线程中,第一想到的是cocos2d-x有没有自带方法,幸运的是咱们找到了CCThread,不幸却发现里面什么都没有。cocos2d-x自带了一个第三方插件--pthread,在cocos2dx\platform\third_party\win32\pthread能够找到。既然是自带的,必须它的理由。想在VS中应用这个插件须要两个步骤:

1.须要右键工程--属性--配置属性--连接器--输入--编缉右侧的附加依赖项--在其中添加pthreadVCE2.lib,以下图所示:

2..须要右键工程--属性--配置属性--C/C++--常规--编缉右侧的附加包含目录--添加新行--找到pthread文件夹所在位置,以下图所示:

而后咱们就能够应用这个插件在程序中开启新线程,简单线程开启方法以下代码所示:

#ifndef _LOADING_SCENE_H__  
#define _LOADING_SCENE_H__  
  
#include "cocos2d.h"  
#include "pthread/pthread.h"  
class LoadingScene : public cocos2d::CCScene{  
public:  
    virtual bool init();  
    CREATE_FUNC(LoadingScene);  
    int start();    
    void update(float dt);  
private:  
    pthread_t pid;  
    static void* updateInfo(void* args); //注意线程函数必须是静态的  
}; 
#include "LoadingScene.h"  
#include "pthread/pthread.h"  
  
using namespace cocos2d;  
bool LoadingScene::init(){  
    this->scheduleUpdate();  
    start();  
    return true;  
}  
void LoadingScene::update(float dt){  
           //能够在这里重绘UI  
}  
void* LoadingScene::updateInfo(void* args){  
      //能够在这里加载资源  
    return NULL;  
}  
int LoadingScene::start(){  
    pthread_create(&pid,NULL,updateInfo,NULL); //开启新线程  
    return 0;  
}  

(2)加载plist

  咱们能够在新开的线程中,加载资源,设置一个静态变量bool,在新线程中,当加载完全部资源后,设置bool值为真。在主线程中Update中,检测bool值,为假,能够重绘UI(例如,显示加载图片,或者模拟加载进度),为真,则加载目标场景。相关代码以下:

void* LoadingScene::updateInfo(void* args){  
     CCSpriteFrameCache *cache = CCSpriteFrameCache::sharedSpriteFrameCache();  
     cache->addSpriteFramesWithFile("BattleIcons.plist");  
     cache->addSpriteFramesWithFile("ArcherAnim.plist");  
     cache->addSpriteFramesWithFile("DeathReaperAnim.plist");  
     loadComplete = true;  //状态值设为真,表示加载完成  
     return NULL;  
}  

  成功加载且运行后,你会发现新场景中全部精灵都不显示(相似于黑屏了)。为何呢?

  由于咱们在加载plist文件时,addSpriteFramesWithFile方法里会帮咱们建立plist对应Png图的Texture2D,并将其加载进缓存中。但是这里就遇到了一个OpenGl规范的问题:不能在新开的线程中,建立texture,texture必须在主线程建立.通俗点,就是全部的opengl api都必须在主线程中调用;其它的操做,好比文件,内存,plist等,能够在新线程中作,这个不是cocos2d不支持,是opengl的标准,无论你是在android,仍是windows上使用opengl,都是这个原理。

  因此不能在新线程中建立Texture2D,致使纹理都不显示,那么该怎么办?让咱们看看CCSpriteFrameCache源码,发现CCSpriteFrameCache::addSpriteFramesWithFile(const char *pszPlist, CCTexture2D *pobTexture)方法,是能够传入Texture2D参数的。是的,咱们找到了解决方法:

int LoadingScene::start(){  
    CCTexture2D *texture = CCTextureCache::sharedTextureCache()->addImage("BattleIcons.png"); //在这里(主线程中)加载plist对应的Png图片进纹理缓存  
    CCTexture2D *texture2 = CCTextureCache::sharedTextureCache()->addImage("ArcherAnim.png"); //以这种方法加载的纹理,其Key值就是文件path值,即例如  
texture2的key值就是ArcherAnim.png  
    CCTexture2D *texture3 = CCTextureCache::sharedTextureCache()->addImage("DeathReaperAnim.png");  
    pthread_create(&pid,NULL,updateInfo,NULL); //开启新线程  
    return 0;  
}  
void* LoadingScene::updateInfo(void* args){  
    CCSpriteFrameCache *cache = CCSpriteFrameCache::sharedSpriteFrameCache();  
    CCTextureCache* teCache = CCTextureCache::sharedTextureCache();     
    CCTexture2D* texture1 = teCache->textureForKey("BattleIcons.png"); //从纹理缓存中取出Texure2D,并将其当参数传入addSpriteFramesWithFile方法中  
    cache->addSpriteFramesWithFile("BattleIcons.plist",texture1);  
    CCTexture2D* texture2 = teCache->textureForKey("ArcherAnim.png");  
    cache->addSpriteFramesWithFile("ArcherAnim.plist",texture2);  
    CCTexture2D* texture3 = teCache->textureForKey("DeathReaperAnim.png");  
    cache->addSpriteFramesWithFile("DeathReaperAnim.plist",texture3);  
    loadComplete = true;  
    return NULL;  
}  

这样解决,就不违背OpenGl规范,没有在新线程中建立Texture2D。

Tip:OpenGL与线程相结合时,此时你须要把你须要渲染的精灵先加载到内存中去,能够设置成为不显示,而后在线程执行后再设置精灵成显示状态,这样能够解决线程与OpneGL渲染不兼容的问题

2、应用实例二——Cocos2d-x 3.0多线程异步资源加载

 【转自】http://tonybai.com/2014/04/28/multithreaded-resource-loading-in-cocos2dx-3/

Cocos2d-x从2.x版本到上周刚刚才发布的Cocos2d-x 3.0 Final版,其引擎驱动核心依旧是一个单线程的“死循环”,一旦某一帧遇到了“大活儿”,好比Size很大的纹理资源加载或网络IO或大量计算,画面将 不可避免出现卡顿以及响应迟缓的现象。从古老的Win32 GUI编程那时起,Guru们就告诉咱们:别阻塞主线程(UI线程),让Worker线程去作那些“大活儿”吧。
 
手机游戏,即使是休闲类的小游戏,每每也涉及大量纹理资源、音视频资源、文件读写以及网络通讯,处理的稍有不甚就会出现画面卡顿,交互不顺畅的状况。虽然引 擎在某些方面提供了一些支持,但有些时候仍是本身祭出Worker线程这个法宝比较灵活,下面就以Cocos2d-x 3.0 Final版游戏初始化为例(针对Android平台),说说如何进行多线程资源加载。
 
咱们常常看到一些手机游戏,启动以后首先会显示一个带有公司Logo的闪屏画面(Flash Screen),而后才会进入一个游戏Welcome场景,点击“开始”才正式进入游戏主场景。而这里Flash Screen的展现环节每每在后台还会作另一件事,那就是加载游戏的图片资源,音乐音效资源以及配置数据读取,这算是一个“障眼法”吧,目的就是提升用 户体验,这样后续场景渲染以及场景切换直接使用已经cache到内存中的数据便可,无需再行加载。
 
(1)为游戏添加FlashScene
在游戏App初始化时,咱们首先建立FlashScene,让游戏尽快显示FlashScene画面:
// AppDelegate.cpp 
bool AppDelegate::applicationDidFinishLaunching() { 
    … … 
    FlashScene* scene = FlashScene::create(); 
    pDirector->runWithScene(scene); 
  
    return true; 
} 

在FlashScene init时,咱们建立一个Resource Load Thread,咱们用一个ResourceLoadIndicator做为渲染线程与Worker线程之间交互的媒介。

//FlashScene.h 
  
struct ResourceLoadIndicator { 
    pthread_mutex_t mutex; 
    bool load_done; 
    void *context; 
}; 
  
class FlashScene : public Scene 
{ 
public: 
    FlashScene(void); 
    ~FlashScene(void); 
  
    virtual bool init(); 
  
    CREATE_FUNC(FlashScene); 
    bool getResourceLoadIndicator(); 
    void setResourceLoadIndicator(bool flag); 
  
private: 
     void updateScene(float dt); 
  
private: 
     ResourceLoadIndicator rli; 
}; 
  
// FlashScene.cpp 
bool FlashScene::init() 
{ 
    bool bRet = false; 
    do { 
        CC_BREAK_IF(!CCScene::init()); 
        Size winSize = Director::getInstance()->getWinSize(); 
  
        //FlashScene本身的资源只能同步加载了 
        Sprite *bg = Sprite::create("FlashSceenBg.png"); 
        CC_BREAK_IF(!bg); 
        bg->setPosition(ccp(winSize.width/2, winSize.height/2)); 
        this->addChild(bg, 0); 
  
        this->schedule(schedule_selector(FlashScene::updateScene) 
                       , 0.01f); 
  
        //start the resource loading thread 
        rli.load_done = false; 
        rli.context = (void*)this; 
        pthread_mutex_init(&rli.mutex, NULL); 
        pthread_attr_t attr; 
        pthread_attr_init(&attr); 
        pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 
        pthread_t thread; 
       pthread_create(&thread, &attr, 
                    resource_load_thread_entry, &rli); 
  
        bRet=true; 
    } while(0); 
  
    return bRet; 
} 
  
static void* resource_load_thread_entry(void* param) 
{ 
    AppDelegate *app = (AppDelegate*)Application::getInstance(); 
    ResourceLoadIndicator *rli = (ResourceLoadIndicator*)param; 
    FlashScene *scene = (FlashScene*)rli->context; 
  
    //load music effect resource 
    … … 
  
    //init from config files 
    … … 
  
    //load images data in worker thread 
    SpriteFrameCache::getInstance()->addSpriteFramesWithFile( // 函数内部会进行纹理建立,不能再非主线程中调用cocos2dx内部函数或egl图形api "All-Sprites.plist"); 
    … … 
  
    //set loading done 
    scene->setResourceLoadIndicator(true); 
    return NULL; 
} 
  
bool FlashScene::getResourceLoadIndicator() 
{ 
    bool flag; 
    pthread_mutex_lock(&rli.mutex); 
    flag = rli.load_done; 
    pthread_mutex_unlock(&rli.mutex); 
    return flag; 
} 
  
void FlashScene::setResourceLoadIndicator(bool flag) 
{ 
    pthread_mutex_lock(&rli.mutex); 
    rli.load_done = flag; 
    pthread_mutex_unlock(&rli.mutex); 
    return; 
} 

咱们在定时器回调函数中对indicator标志位进行检查,当发现加载ok后,切换到接下来的游戏开始场景: 

void FlashScene::updateScene(float dt) 
{ 
    if (getResourceLoadIndicator()) { 
        Director::getInstance()->replaceScene( 
                              WelcomeScene::create()); 
    } 
}

到此,FlashScene的初始设计和实现完成了。Run一下试试吧。

(2)崩溃
在GenyMotion的4.4.2模拟器上,游戏运行的结果并无如我指望,FlashScreen显现后游戏就异常崩溃退出了。经过monitor分析游戏的运行日志,咱们看到了以下一些异常日志: 
threadid=24: thread exiting, not yet detached (count=0) 
threadid=24: thread exiting, not yet detached (count=1) 
threadid=24: native thread exited without detaching 

非常奇怪啊,咱们在建立线程时,明明设置了 PTHREAD_CREATE_DETACHED属性了啊:

pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 
怎么还会出现这个问题,并且竟然有三条日志。翻看了一下引擎内核的代码TextureCache::addImageAsync,在线程建立以及线程主函数中也没有发现什么特别的设置。为什么内核能够建立线程,我本身建立就会崩溃呢。Debug多个来回,问题彷佛聚焦在resource_load_thread_entry中执行的任务。在个人代码里,我利用SimpleAudioEngine加载了音效资源、利用UserDefault读取了一些持久化的数据,把这两个任务去掉,游戏就会进入到下一个环节而不会崩溃。
SimpleAudioEngine和UserDefault能有什么共同点呢?Jni调用。没错,这两个接口底层要适配多个平台,而对于Android 平台,他们都用到了Jni提供的接口去调用Java中的方法。而 Jni对多线程是有约束的。Android开发者官网上有这么一段话:
 
  All threads are Linux threads, scheduled by the kernel. They're usually started from managed code (using Thread.start), but they can also be created elsewhere and then attached to the JavaVM. For example, a thread started with pthread_create can be attached with the JNI AttachCurrentThread or AttachCurrentThreadAsDaemon functions. Until a thread is attached, it has no JNIEnv, and cannot make JNI calls.
 
由此看来 pthread_create建立的新线程默认状况下是不能进行Jni接口调用的,除非Attach到Vm,得到一个JniEnv对象,而且在线程exit前要Detach Vm。好,咱们来尝试一下,Cocos2d-x引擎提供了一些JniHelper方法,能够方便进行Jni相关操做。
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) 
#include "platform/android/jni/JniHelper.h" 
#include <jni.h> 
#endif 
  
static void* resource_load_thread_entry(void* param) 
{ 
    … … 
  
    JavaVM *vm; 
    JNIEnv *env; 
    vm = JniHelper::getJavaVM(); 
  
    JavaVMAttachArgs thread_args; 
  
    thread_args.name = "Resource Load"; 
    thread_args.version = JNI_VERSION_1_4; 
    thread_args.group = NULL; 
  
    vm->AttachCurrentThread(&env, &thread_args); 
    … … 
    //Your Jni Calls 
    … … 
  
    vm->DetachCurrentThread(); 
    … … 
    return NULL; 
} 

关于什么是JavaVM,什么是JniEnv,Android Developer官方文档中是这样描述的

  The JavaVM provides the "invocation interface" functions, which allow you to create and destroy a JavaVM. In theory you can have multiple JavaVMs per process, but Android only allows one.

  The JNIEnv provides most of the JNI functions. Your native functions all receive a JNIEnv as the first argument.
  The JNIEnv is used for thread-local storage. For this reason, you cannot share a JNIEnv between threads.
 
(3) 黑屏

上面的代码成功解决了线程崩溃的问题,但问题还没完,由于接下来咱们又遇到了“黑屏”事件。所谓的“黑屏”,其实并非全黑。但进入游戏 WelcomScene时,只有Scene中的LabelTTF实例能显示出来,其他Sprite都没法显示。显然确定与咱们在Worker线程加载纹理资源有关了: 

libEGL: call to OpenGL ES API with no current context (logged once per thread)
  经过Google得知,只有Renderer Thread才能进行egl调用,由于egl的context是在Renderer Thread建立的,Worker Thread并无EGL的context,在进行egl操做时,没法找到context,所以操做都是失败的,纹理也就没法显示出来。要解决这个问题就 得查看一下TextureCache::addImageAsync是如何作的了。
   TextureCache::addImageAsync只是在worker线程进行了image数据的加载,而纹理对象Texture2D instance则是在addImageAsyncCallBack中建立的。也就是说纹理仍是在Renderer线程中建立的,所以不会出现咱们上面的 “黑屏”问题。模仿addImageAsync,咱们来修改一下代码:
static void* resource_load_thread_entry(void* param) 
{ 
    … … 
    allSpritesImage = new Image(); 
    allSpritesImage->initWithImageFile("All-Sprites.png"); 
    … … 
} 
  
void FlashScene::updateScene(float dt) 
{ 
    if (getResourceLoadIndicator()) { 
        // construct texture with preloaded images 
        Texture2D *allSpritesTexture = TextureCache::getInstance()-> 
                           addImage(allSpritesImage, "All-Sprites.png"); 
        allSpritesImage->release(); 
        SpriteFrameCache::getInstance()->addSpriteFramesWithFile( 
                           "All-Sprites.plist", allSpritesTexture); 
      
        Director::getInstance()->replaceScene(WelcomeScene::create()); 
    } 
} 

完成这一修改后,游戏画面就变得一切正常了,多线程资源加载机制正式生效。

------------------------------------------------------------------------------------------------

CocoaChina是全球最大的苹果开发中文社区.

 

 (6)cocos2dx内存管理与多线程问题

【转自】http://blog.csdn.net/kaitiren/article/details/14453313

  Cocos2d-x的内存管理采用Objective-C的机制,大喜过望。由于只要坚持Objective-C的原则“谁建立谁释放,谁备份谁释放”的原则便可确保内存使用不易出现Bug。
   可是由于游戏须要使用到多线程技术,致使测试的时候老是莫名其妙的致使空指针错误。并且是随机出现,纠结了2天无果后,开始怀疑Cocos2d-X的内 存自己管理可能存在问题。怀着这样的想法,一步一步的调试,发现常常出现指针异常的变量老是在调用autorelease一会后,再使用的时候就莫名其妙 抛异常。狠下心,在它的析构函数里面断点+Log输出信息。发现对象被释放了。一时也很迷糊,由于对象只是autorelease,并无真正释放,是谁 致使它释放的?

而后就去看了CCAutoreleasePool的源码,发现Cocos2d-X的内存管理在多线程的状况下存在以下问题:

   如图:thread 1和thread 2是独立的两个线程,它们之间存在CPU分配的交叉集,咱们在time 1的时候push一个autorelease的自动释放池,在该线程的末尾,即time 3的时候pop它。同理在thread 2的线程里面,在time 2的时候push一个自动释放池,在time 4的时候释放它,即Pop.

  此时咱们假设在thread 2分配获得CPU的时候有一个对象obj自动释放(在多线程下,这种状况是有可能发生的,A线程push了一个对象,而B线程执行autorelease时,会把A线程的对象提早释放), 即obj-autorelease().那么在time 3的时候会发生是么事情呢?答案很简单,就是obj在time 3的时候就被释放了,而咱们指望它在time 4的时候才释放。因此就致使我上面说的,在多线程下面,cocos2d-x的autorelease变量会发生莫名其妙的指针异常。

  解决方法:在PoolManager给每一个线程根据pthread_t的线程id生成一个CCArray的stack的嵌套管理自动释放池。在Push的时 候根据当前线程的pthread_t的线程id生成一个CCArray的stack来存储该线程对应的Autoreleasepool的嵌套对象。

相关文章
相关标签/搜索