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的嵌套對象。

相關文章
相關標籤/搜索