找回密码
 立即注册
查看: 892|回复: 5

[简易教程] cocos 新线程使用pthread加载plist

[复制链接]
发表于 2019-2-18 16:11 | 显示全部楼层 |阅读模式
原文:https://www.cnblogs.com/yyxt/p/4087123.html
一、多线程原理
(1)单线程的尴尬
  重新回顾下 Cocos2d-x 的并行机制。引擎内部实现了一个庞大的主循环,在每帧之间更新各个精灵的状态、执行动作、调用定时函数等,这些操作之间可以保证严格独立,互不干扰。不得不说,这是一个非常巧妙的机制,它用一个线程就实现了并发,尤其是将连续的动作变化切割为离散的状态更新时,利用帧间间隔刷新这些状态即实现了多个动作的模拟。
  但这在本质上毕竟是一个串行的过程,一种尴尬的场景是,我们需要执行一个大的计算任务,两帧之间几十毫秒的时间根本不可能完成,例如加载几十张图片到内存中,这时候引擎提供的 schedule 并行就显得无力了:一次只能执行一个小时间片,我们要么将任务进一步细分为一个个更小的任务,要么只能眼睁睁地看着屏幕上的帧率往下掉,因为这个庞大计算消耗了太多时间,阻塞了主循环的正常运行。
  本来这个问题是难以避免的,但是随着移动设备硬件性能的提高,双核甚至四核的机器已经越来越普遍了,如果再不通过多线程挖掘硬件潜力就过于浪费了。
(2)pthead
  pthread 是一套 POSIX 标准线程库,可以运行在各个平台上,包括 Android、iOS 和 Windows,也是 Cocos2d-x 官方推荐的多线程库。它使用 C 语言开发,提供非常友好也足够简洁的开发接口。一个线程的创建通常是这样的:
[url=][/url]
void* justAnotherTest(void *arg){    LOG_FUNCTION_LIFE;    //在这里写入新线程将要执行的代码    return NULL;}void testThread(){    LOG_FUNCTION_LIFE;    pthread_t tid;    pthread_create(&tid, NULL, &justAnotherTest, NULL);}[url=][/url]

  这里我们在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创建精灵的时候,上面的线程还没有执行或者没有执行完成图片资源的加载,这时就可能出现找不到图片。
[url=][/url]
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");}[url=][/url]

  在新的线程中对缓存的调用所产生的一系列内存管理操作更可能导致系统崩溃。
  因此,使用多线程的首要原则是,在新建立的线程中不要使用任何 Cocos2d-x 内建的内存管理,也不要调用任何引擎提供的函数或方法,因为那可能会导致 Cocos2d-x 内存管理错误。
  同样,OpenGL 的各个接口函数也不是线程安全的。也就是说,一切和绘图直接相关的操作都应该放在主线程内执行,而不是在新建线程内执行。(见第六点cocos2dx内存管理与多线程问题)
(4)线程间任务安排
  使用并发编程的最直接目的是保证界面流畅,这也是引擎占据主线程的原因。因此,除了界面相关的代码外,其他操作都可以放入新的线程中执行,主要包括文件读写和网络通信两类。
  文件读写涉及外部存储操作,这和内存、CPU 都不在一个响应级别上。如果将其放入主线程中,就可能会造成阻塞,尤为严重的是大型图片的载入。对于碎图压缩后的大型纹理和高分辨率的背景图,一次加载可能耗费 0.2 s 以上的时间,如果完全放在主线程内,会阻塞主线程相当长的时间,导致画面停滞,游戏体验很糟糕。在一些大型的卷轴类游戏中,这类问题尤为明显。考虑到这个问题,Cocos2d-x 为我们提供了一个异步加载图片的接口,不会阻塞主线程,其内部正是采用了新建线程的办法。
  我们用游戏中的背景层为例,原来加载背景层的操作是串行的,相关代码如下:
[url=][/url]
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;}[url=][/url]

  现在我们将这一些列串行的过程分离开来,使用引擎提供的异步加载图片接口异步加载图片,相关代码如下:
[url=][/url]
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;}[url=][/url]

  为了加强效果的对比,我们在图片加载成功后,延时了 2 s,而后才真正加载背景图片到背景层中。读者可以明显看到,2s后游戏中才出现了背景图。尽管引擎已经为我们提供了异步加载图片缓存的方式,但考虑到对图片资源的加密解密过程是十分耗费计算资源的,我们还是有必要单开一个线程执行这一系列操作。另一个值得使用并发编程的是网络通信。网络通信可能比文件读写要慢一个数量级。一般的网络通信库都会提供异步传输形式,我们只需要注意选择就好。
(5)线程同步
使用了线程,必然就要考虑到线程同步,不同的线程同时访问资源的话,访问的顺序是不可预知的,会造成不可预知的结果。查看addImageAsync的实现源码可以知道它是使用pthread_mutex_t来实现同步:
[url=][/url]
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);}[url=][/url]


二、应用实例一——cococs2d-x 多线程加载plist
(1)环境搭建
当我们想在程序中开多线程中,第一想到的是cocos2d-x有没有自带方法,幸运的是我们找到了CCThread,不幸却发现里面什么都没有。cocos2d-x自带了一个第三方插件--pthread,在cocos2dx\platform\third_party\win32\pthread可以找到。既然是自带的,必须它的理由。想在VS中应用这个插件需要两个步骤:
1.需要右键工程--属性--配置属性--链接器--输入--编缉右侧的附加依赖项--在其中添加pthreadVCE2.lib,如下图所示:
2..需要右键工程--属性--配置属性--C/C++--常规--编缉右侧的附加包含目录--添加新行--找到pthread文件夹所在位置,如下图所示:
然后我们就可以应用这个插件在程序中开启新线程,简单线程开启方法如下代码所示:
[url=][/url]
#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); //注意线程函数必须是静态的  }; [url=][/url]

[url=][/url]
#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;  }  [url=][/url]

(2)加载plist
  我们可以在新开的线程中,加载资源,设置一个静态变量bool,在新线程中,当加载完所有资源后,设置bool值为真。在主线程中Update中,检测bool值,为假,可以重绘UI(例如,显示加载图片,或者模拟加载进度),为真,则加载目标场景。相关代码如下:
[url=][/url]
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;  }  [url=][/url]

  成功加载且运行后,你会发现新场景中所有精灵都不显示(类似于黑屏了)。为什么呢?
  因为我们在加载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参数的。是的,我们找到了解决方法:
[url=][/url]
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;  }  [url=][/url]

这样解决,就不违背OpenGl规范,没有在新线程中创建Texture2D。
Tip:OpenGL与线程相结合时,此时你需要把你需要渲染的精灵先加载到内存中去,可以设置成为不显示,然后在线程执行后再设置精灵成显示状态,这样可以解决线程与OpneGL渲染不兼容的问题
二、应用实例二——Cocos2d-x 3.0多线程异步资源加载
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画面:
[url=][/url]
// AppDelegate.cpp bool AppDelegate::applicationDidFinishLaunching() {     … …     FlashScene* scene = FlashScene::create();     pDirector->runWithScene(scene);       return true; } [url=][/url]

在FlashScene init时,我们创建一个Resource Load Thread,我们用一个ResourceLoadIndicator作为渲染线程与Worker线程之间交互的媒介。
[url=][/url]
//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; } [url=][/url]

我们在定时器回调函数中对indicator标志位进行检查,当发现加载ok后,切换到接下来的游戏开始场景:
[url=][/url]
void FlashScene::updateScene(float dt) {     if (getResourceLoadIndicator()) {         Director::getInstance()->replaceScene(                               WelcomeScene::create());     } }[url=][/url]

到此,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相关操作。
[url=][/url]
#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; } [url=][/url]

关于什么是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,我们来修改一下代码:
[url=][/url]
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());     } } [url=][/url]

完成这一修改后,游戏画面就变得一切正常了,多线程资源加载机制正式生效。
------------------------------------------------------------------------------------------------
CocoaChina是全球最大的苹果开发中文社区.

(6)cocos2dx内存管理与多线程问题
  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的嵌套对象。

发表于 2019-3-1 08:39 | 显示全部楼层
楼主是超人
发表于 2019-3-1 08:29 | 显示全部楼层
顶顶多好
发表于 2019-3-1 08:16 | 显示全部楼层
真心顶
发表于 2019-3-1 08:44 | 显示全部楼层
难得一见的好帖
发表于 2019-3-1 08:34 | 显示全部楼层
LZ真是人才
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-29 16:44 , Processed in 0.121012 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表