cocos2d-x游戏引擎核心之多线程分析及注意事项

6 篇文章 0 订阅
3 篇文章 0 订阅

转载自:http://www.cnblogs.com/yyxt/p/4087123.html

一、多线程原理

(1)单线程的尴尬

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

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

  本来这个问题是难以避免的,但是随着移动设备硬件性能的提高,双核甚至四核的机器已经越来越普遍了,如果再不通过多线程挖掘硬件潜力就过于浪费了。

(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)线程安全

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

在 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);
}
复制代码

 

二、应用实例一——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渲染不兼容的问题

二、应用实例二——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的嵌套对象。


cocos2d-x游戏引擎核心之——并发编程(消息通知中心)

这里介绍cocos2d-x的一种消息/数据传递方式,内置的观察者模式,也称消息通知中心CCNotificationCenter

  虽然引擎没有为我们封装线程类,但还是提供了一些组件,辅助我们进行并发编程。除了上面提到的异步加载图片,引擎还提供了消息中心 CCNotificationCenter。这是一个类似 Qt 中消息槽的机制,一个对象可以注册到消息中心,指定要接收的消息;而某个事件完成时,则可以发送对应的消息,之前注册过的对象会得到通知。主要有以下两个关键的接口函数:

void addObserver(CCObject *target, //接收消息的对象
                SEL_CallFuncO selector, //响应消息的函数
                const char *name, //待接收的消息
                CCObject *obj); //指定消息的发送者,目前暂时为无用参数
void postNotification(const char *name); //发送一个消息

  借助消息中心,异步事件之间的对象可以进一步减少耦合,使用事件驱动的方式编写代码。以游戏中的金币数变动为例,我们将菜单层添加为金币数量变化的消息的观察者,相关代码如下:

CCNotificationCenter::sharedNotificationCenter()->addObserver(this,callfuncO_selector(GameMenuLayer::coinChange), "CoinChange", NULL);

然后在开炮、捕获鱼等引起金币变化的地方发出该消息,从而触发菜单层的 coinChange 函数:

CCNotificationCenter::sharedNotificationCenter()->postNotification("CoinChange", NULL);

  当然,在多线程的环境中,考虑到之前提到的原则,不可能直接在分离的线程中调用消息中心发送消息,我们可以建立一个线程间共享的消息池,让消息可以在不同线程间流动,或者说,我们需要建立一个线程安全的消息队列。下面我们创建一个线程安全的消息队列,代码如下:

复制代码
class MTNotificationQueue : CCNode
{
    typedef struct
    {
        string name;
        CCObject* object;
    } NotificationArgs;
    vector<NotificationArgs> notifications;
    MTNotificationQueue(void);
public:
    static MTNotificationQueue* sharedNotificationQueue();
    void postNotifications(ccTime dt);
    ~MTNotificationQueue(void);
    void postNotification(const char* name, CCObject* object);
};
复制代码

  从接口上看,这个消息队列可以看做引擎自带的消息中心的补充,因为这里并不提供消息接收者的注册,仅仅是允许线程安全地向消息中心发出一个消息样也对应了一种处理模式:主线程负责绘图实现,在分离出来的子线程中完成重计算任务,计算完成后向主线程发回处理完毕的消息,消息是单向流动的,数据从磁盘、网络或其他任何地方经过处理后最终以视图的形式流向了屏幕。
  在实现上,我们通过一个数组缓冲了各线程间提交的消息,稍后在主线程中将这些消息一次性地向 CCNotificationCenter发出。其中需要保证的是,缓冲用的数组在不同线程间的访问必须是安全的,因此需要一个互斥锁

不同线程间可共享的数据必须是静态的或全局的,因此互斥锁也必须是全局的。考虑到这个消息队列应该是全局唯一的单例,仅仅需要一个全局唯一的互斥锁与之对应

pthread_mutex_t sharedNotificationQueueLock;

而考虑到这个互斥锁必须进行合适的初始化和清理,可以用一个类的全局变量管理其生命周期:

复制代码
class LifeManager_PThreadMutex
{
    pthread_mutex_t* mutex;
public:
    LifeManager_PThreadMutex(pthread_mutex_t* mut) : mutex(mut)
    {
        pthread_mutex_init(mutex, NULL);
    }
    ~LifeManager_PThreadMutex()
    {
        pthread_mutex_destroy(mutex);
    }
}__LifeManager_sharedNotificationQueueLock(&sharedNotificationQueueLock);
复制代码

在 pthread 库中,我们使用下面一对函数进行互斥锁的上锁和解锁:

int pthread_mutex_lock (pthread_mutex_t * mutex); //上锁
int pthread_mutex_unlock (pthread_mutex_t * mutex); //解锁

这里的上锁函数是阻塞性的,如果目标互斥锁已经被锁上,会一直阻塞线程直到解锁,然后再次尝试解锁直到成功从当前线程上锁为止。

同样,考虑到上锁过程往往对应了一段函数或一个程序段的开始和结束,可以对应到一个临时变量的生命周期中,我们再次封装一个"生命周期锁类":

复制代码
class LifeCircleMutexLocker
{
    pthread_mutex_t* mutex;
public:
    LifeCircleMutexLocker(pthread_mutex_t* aMutex) : mutex(aMutex)
    {
        pthread_mutex_lock(mutex);
    }
    ~LifeCircleMutexLocker(){
        pthread_mutex_unlock(mutex);
    }
};
#define LifeCircleMutexLock(mutex) LifeCircleMutexLocker __locker__(mutex)
复制代码

一切准备就绪后,就剩下两个核心的接口函数--向队列发出消息以及由队列将消息发到消息中心中,相关代码如下:

复制代码
//由队列将消息发到消息中心
void MTNotificationQueue::postNotifications(ccTime dt)
{
  //生命周期锁
  // 用一个类LifeCircleMutexLock管理互斥锁sharedNotificationQueueLock的生命周期
LifeCircleMutexLock(&sharedNotificationQueueLock);
  
for(int i = 0; i < notifications.size(); i++) { NotificationArgs &arg = notifications[i];      // 调用主线程通知函数,将所有消息发送到消息中心
CCNotificationCenter::sharedNotificationCenter()
->postNotification(arg.name.c_str(), arg.object); } notifications.clear(); } // 向队列发出消息 void MTNotificationQueue::postNotification(const char* name, CCObject* object) { //生命周期锁 LifeCircleMutexLock(&sharedNotificationQueueLock); NotificationArgs arg; arg.name = name; if(object != NULL)   arg.object = object->copy(); else   arg.object = NULL; notifications.push_back(arg); }
复制代码

  实际上,这是两个非常简短的函数,仅仅是将传入的消息缓冲到数组中并取出。唯一的特别之处只在于函数在开始时,使用了我们前面定义的"生命周期锁",保证了在访问缓冲数组的过程中是线程安全的,整个读写过程中缓冲数组由当前线程独占。

最后,我们启动消息队列的定时器,使 postNotifications 函数每帧被调用,保证不同线程间发出的消息能第一时间送达主线程:

CCDirector::sharedDirector()->getScheduler()->scheduleSelector(
            schedule_selector(MTNotificationQueue::postNotifications),
            MTNotificationQueue::sharedNotificationQueue(),
            1.0 / 60.0,
            false);

有了这个消息池,就可以进一步简化之前的图片加载过程了。下面仍然使用背景层的例子,再次重写游戏背景层的初始化函数:

复制代码
bool BackgroundLayer::init()
{
    LOG_FUNCTION_LIFE;
    bool bRet = false;
    do {
        CC_BREAK_IF(! CCLayer::init());
        CCNotificationCenter::sharedNotificationCenter()->addObserver(
                            this,
                            callfuncO_selector(BackgroundLayer::loadImageFinish),
                            "loadImageFinish",
                            NULL);
        pthread_t tid;
        pthread_create(&tid, NULL, &loadImages, NULL);
        bRet = true;
    } while (0);
    return bRet;
}
复制代码

  我们不再按照注释中的做法那样使用系统的纹理缓存来异步添加背景图片,而是先注册到消息中心,而后主动创建一个线程负责加载图片。在该线程中,我们仅完成图片向内存的加载,相关代码如下:

复制代码
void* loadImages(void* arg)
{
    bgImage = new CCImage();
    bgImage->initWithImageFileThreadSafe("background.png");
    MTNotificationQueue::sharedNotificationQueue()->postNotification("loadImageFinish", NULL);
    return NULL;
}
复制代码

  在加载完成之后,我们通过消息队列发出了一个加载完成的消息,在稍后的消息队列更新时,这个消息将会被发送到消息中心,而后通知到背景层的响应函数中。我们为背景层添加相应的响应函数 loadImageFinish,其代码如下:

复制代码
void BackgroundLayer::loadImageFinish(CCObject* sender)
{
    CCSize winSize = CCDirector::sharedDirector()->getWinSize();
    CCTexture2D* texture = CCTextureCache::sharedTextureCache()->addUIImage(bgImage, "background.png");
    bgImage->release();
    CCSprite* bg = CCSprite::create(texture);
    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);
}
复制代码

这里 bgImage 是用 new 方式创建的,堆空间是除了全局静态对象之外唯一可以在线程间共享的数据空间。
  必须注意的是,作为共享数据的 bgImage 的内存管理方式,我们在加载线程中用 new 从堆空间中分配了该内存,但是并没有遵守内存管理规范在该函数中将其释放,因为此时 bgImage 还未使用完毕,也不可能调用自动释放池,因为在子线程中是不存在自动释放池的,如果跨线程调用了自动释放池,将造成严重的紊乱。因此,我们最后在 loadimageFinish 中添加到纹理缓存后才将其释放。
  这也是使用多线程进行并发编程时的一个比较大的障碍,由于引擎的内存管理体系 CCObject 是非线程安全的,而整个引擎又是搭建在 CCObject 提供的内存管理机制基础上的,因此我们在多线程环境中使用引擎的任何对象都必须分外小心。

二、cocos2dx消息中心:

自行查阅相关源码, 没有使用多线程.

使用CCNotificationCenter需要注意以下几点:

(1)一个对象可以注册多个消息,一个消息也可以由多个消息注册。

(2)传递参数,A可以向B传递参数,而B在注册的时候也可以带一个参数,如果这两个数据不是指向同一对象的话,消息不会传递。也就是说要么A传递NULL对象,要么B注册时带NULL对象,要么都不是NULL但必须是同一对象,消息传递才会成功。以下是发送消息执行的判断:

if (!strcmp(name,observer->getName()) && (observer->getObject() == object || observer->getObject() == NULL || object == NULL))  

(3)局部变量的传递,注意到上例,传递的是CCString的一个局部变量(但还是要autorelease),从CCNotificationCenter的实现上来看,这是没有问题的,因为数据是在postNotification被调用的,也就是整个函数体并没结束,数据不会被销毁。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: cocos2d-x是一款流行的开源游戏引擎,它可以用作开发本地移动游戏和桌面游戏。利用cocos2d-x游戏源码,开发人员可以快速构建流畅、高效、具有吸引力的游戏。该引擎使用C ++语言和脚本语言Lua来实现游戏开发,用户可以根据自己的需要进行选择。 cocos2d-x游戏源码包含许多强大的功能和工具,例如精灵、动画、场景管理、碰撞检测和音频控制等。通过这些功能,开发人员可以方便地创建各种类型的游戏,例如动作游戏、射击游戏、冒险游戏和益智游戏等。 此外,cocos2d-x游戏源码还具有高度定制化的特性,允许开发人员根据他们的需求自定义游戏元素。这种定制化可以通过改进游戏画面,添加新的模式和关卡,甚至整个新的游戏玩法来完成。 总之,cocos2d-x游戏源码是一款功能强大、易于使用和高度定制的游戏引擎,它可以帮助开发人员快速开发各种类型的游戏。无论你是专业开发人员还是无经验的游戏制作者,cocos2d-x都是一个极好的选择。 ### 回答2: Cocos2d-x游戏源码是使用Cocos2d-x引擎进行开发的游戏程序代码。Cocos2d-x引擎是一个开源的跨平台游戏引擎,可以支持iOS、Android、Windows Phone、Windows、MacOS和Linux等多个平台,并且提供了丰富的游戏开发API和工具集。Cocos2d-x游戏源码包含了游戏的核心逻辑、UI设计、动画效果、音频效果等各方面的代码,可以作为开发者学习和借鉴的重要资料。 由于Cocos2d-x引擎支持多种编程语言(如C++、Lua等),因此Cocos2d-x游戏源码也有对应的编程语言版本。开发者可以通过阅读Cocos2d-x游戏源码,了解游戏开发过程中的技术细节,学习如何使用Cocos2d-x引擎进行游戏开发。同时,开发者也可以通过对Cocos2d-x游戏源码进行修改和优化,来满足游戏的特定需求,提升游戏的性能和用户体验。 总之,Cocos2d-x游戏源码是游戏开发者必备的重要资源,可以帮助开发者更好地了解游戏开发技术,提升游戏开发水平。 ### 回答3: cocos2d-x是一款优秀的游戏开发引擎,其源码包含了许多功能强大的游戏开发工具和API。使用cocos2d-x可以帮助开发者更快速更高效地开发出优秀的游戏作品。 cocos2d-x的游戏源码非常丰富,其包含了许多不同类型的游戏示例和模板,如平面射击、塔防、解谜等,这些示例可以帮助开发者更好地了解cocos2d-x的使用方法和原理。 cocos2d-x的源码还包含了许多强大的API和工具,例如场景管理、动画控制、音频引擎等,这些工具和API能够帮助开发者更好地完成游戏的开发和调试。 此外,cocos2d-x的源码还提供了完整的游戏开发框架,包括资源管理、内存管理、事件机制等,这些框架可以帮助开发者更好地组织代码和提高代码的可维护性。 总的来说,cocos2d-x游戏源码提供了丰富的工具和API,可以帮助开发者更高效地进行游戏开发,大大降低了开发难度和成本,是一款非常适合游戏开发者使用的引擎。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值