Cocos2d-x 3.0多線程異步加載資源實(shí)例
Cocos2d-x從2.x版本到上周剛剛才發(fā)布的Cocos2d-x 3.0 Final版,其引擎驅(qū)動(dòng)核心依舊是一個(gè)單線程的“死循環(huán)”,一旦某一幀遇到了“大活兒”,比如Size很大的紋理資源加載或網(wǎng)絡(luò)IO或大量計(jì)算,畫面將 不可避免出現(xiàn)卡頓以及響應(yīng)遲緩的現(xiàn)象。從古老的Win32 GUI編程那時(shí)起,Guru們就告訴我們:別阻塞主線程(UI線程),讓W(xué)orker線程去做那些“大活兒”吧。
手機(jī)游戲,即便是休閑類的小游戲,往往也涉及大量紋理資源、音視頻資源、文件讀寫以及網(wǎng)絡(luò)通信,處理的稍有不甚就會(huì)出現(xiàn)畫面卡頓,交互不暢的情況。雖然引擎在某些方面提供了一些支持,但有些時(shí)候還是自己祭出Worker線程這個(gè)法寶比較靈活,下面就以Cocos2d-x 3.0 Final版游戲初始化為例(針對(duì)Android平臺(tái)),說(shuō)說(shuō)如何進(jìn)行多線程資源加載。
我們經(jīng)??吹揭恍┦謾C(jī)游戲,啟動(dòng)之后首先會(huì)顯示一個(gè)帶有公司Logo的閃屏畫面(Flash Screen),然后才會(huì)進(jìn)入一個(gè)游戲Welcome場(chǎng)景,點(diǎn)擊“開始”才正式進(jìn)入游戲主場(chǎng)景。而這里Flash Screen的展示環(huán)節(jié)往往在后臺(tái)還會(huì)做另外一件事,那就是加載游戲的圖片資源,音樂(lè)音效資源以及配置數(shù)據(jù)讀取,這算是一個(gè)“障眼法”吧,目的就是提高用 戶體驗(yàn),這樣后續(xù)場(chǎng)景渲染以及場(chǎng)景切換直接使用已經(jīng)cache到內(nèi)存中的數(shù)據(jù)即可,無(wú)需再行加載。
一、為游戲添加FlashScene
在游戲App初始化時(shí),我們首先創(chuàng)建FlashScene,讓游戲盡快顯示FlashScene畫面:
// AppDelegate.cpp
bool AppDelegate::applicationDidFinishLaunching() {
… …
FlashScene* scene = FlashScene::create();
pDirector->runWithScene(scene);
return true;
}
在FlashScene init時(shí),我們創(chuàng)建一個(gè)Resource Load Thread,我們用一個(gè)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(
"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;
}
我們?cè)诙〞r(shí)器回調(diào)函數(shù)中對(duì)indicator標(biāo)志位進(jìn)行檢查,當(dāng)發(fā)現(xiàn)加載ok后,切換到接下來(lái)的游戲開始場(chǎng)景:
void FlashScene::updateScene(float dt)
{
if (getResourceLoadIndicator()) {
Director::getInstance()->replaceScene(
WelcomeScene::create());
}
}
到此,F(xiàn)lashScene的初始設(shè)計(jì)和實(shí)現(xiàn)完成了。Run一下試試吧。
二、解決崩潰問(wèn)題
在GenyMotion的4.4.2模擬器上,游戲運(yùn)行的結(jié)果并沒(méi)有如我期望,F(xiàn)lashScreen顯現(xiàn)后游戲就異常崩潰退出了。
通過(guò)monitor分析游戲的運(yùn)行日志,我們看到了如下一些異常日志:
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
很是奇怪啊,我們?cè)趧?chuàng)建線程時(shí),明明設(shè)置了 PTHREAD_CREATE_DETACHED屬性了?。?/P>
怎么還會(huì)出現(xiàn)這個(gè)問(wèn)題,而且居然有三條日志。翻看了一下引擎內(nèi)核的代碼TextureCache::addImageAsync,在線程創(chuàng)建以及線程主函數(shù)中也沒(méi)有發(fā)現(xiàn)什么特別的設(shè)置。為何內(nèi)核可以創(chuàng)建線程,我自己創(chuàng)建就會(huì)崩潰呢。Debug多個(gè)來(lái)回,問(wèn)題似乎聚焦在resource_load_thread_entry中執(zhí)行的任務(wù)。在我的代碼里,我利用SimpleAudioEngine加載了音效資源、利用UserDefault讀取了一些持久化的數(shù)據(jù),把這兩個(gè)任務(wù)去掉,游戲就會(huì)進(jìn)入到下一個(gè)環(huán)節(jié)而不會(huì)崩潰。
SimpleAudioEngine和UserDefault能有什么共同點(diǎn)呢?Jni調(diào)用。沒(méi)錯(cuò),這兩個(gè)接口底層要適配多個(gè)平臺(tái),而對(duì)于Android 平臺(tái),他們都用到了Jni提供的接口去調(diào)用Java中的方法。而Jni對(duì)多線程是有約束的。Android開發(fā)者官網(wǎng)上有這么一段話:
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.
由此看來(lái)pthread_create創(chuàng)建的新線程默認(rèn)情況下是不能進(jìn)行Jni接口調(diào)用的,除非Attach到Vm,獲得一個(gè)JniEnv對(duì)象,并且在線 程exit前要Detach Vm。好,我們來(lái)嘗試一下,Cocos2d-x引擎提供了一些JniHelper方法,可以方便進(jìn)行Jni相關(guān)操作。
#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;
}
關(guān)于什么是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.
三、解決黑屏問(wèn)題
上面的代碼成功解決了線程崩潰的問(wèn)題,但問(wèn)題還沒(méi)完,因?yàn)榻酉聛?lái)我們又遇到了“黑屏”事件。所謂的“黑屏”,其實(shí)并不是全黑。但進(jìn)入游戲 WelcomScene時(shí),只有Scene中的LabelTTF實(shí)例能顯示出來(lái),其余Sprite都無(wú)法顯示。顯然肯定與我們?cè)赪orker線程加載紋理 資源有關(guān)了:
我們通過(guò)碎圖壓縮到一張大紋理的方式建立SpriteFrame,這是Cocos2d-x推薦的優(yōu)化手段。但要想找到這個(gè)問(wèn)題的根源,還得看monitor日志。我們的確發(fā)現(xiàn)了一些異常日志:
通過(guò)Google得知,只有Renderer Thread才能進(jìn)行egl調(diào)用,因?yàn)閑gl的context是在Renderer Thread創(chuàng)建的,Worker Thread并沒(méi)有EGL的context,在進(jìn)行egl操作時(shí),無(wú)法找到context,因此操作都是失敗的,紋理也就無(wú)法顯示出來(lái)。要解決這個(gè)問(wèn)題就 得查看一下TextureCache::addImageAsync是如何做的了。
TextureCache::addImageAsync只是在worker線程進(jìn)行了image數(shù)據(jù)的加載,而紋理對(duì)象Texture2D instance則是在addImageAsyncCallBack中創(chuàng)建的。也就是說(shuō)紋理還是在Renderer線程中創(chuàng)建的,因此不會(huì)出現(xiàn)我們上面的 “黑屏”問(wèn)題。模仿addImageAsync,我們來(lái)修改一下代碼:
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());
}
}
完成這一修改后,游戲畫面就變得一切正常了,多線程資源加載機(jī)制正式生效。
相關(guān)文章
Android中發(fā)送Http請(qǐng)求(包括文件上傳、servlet接收)的實(shí)例代碼
首先我是寫了個(gè)java工程測(cè)試發(fā)送post請(qǐng)求:可以包含文本參數(shù)和文件參數(shù)2013-05-05Android程序結(jié)構(gòu)簡(jiǎn)單講解
在本篇文章里小編給大家分享一篇關(guān)于Android程序結(jié)構(gòu)的簡(jiǎn)單說(shuō)明內(nèi)容,有需要的朋友們跟著學(xué)習(xí)下。2019-02-02Android 8.0實(shí)現(xiàn)發(fā)送通知
這篇文章主要為大家詳細(xì)介紹了Android 8.0實(shí)現(xiàn)發(fā)送通知,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-07-07Android實(shí)現(xiàn)底部半透明彈出框PopUpWindow效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)底部半透明彈出框PopUpWindow效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07Android Studio 4.0新特性及升級(jí)異常問(wèn)題的解決方案
這篇文章主要介紹了Android Studio 4.0新特性及升級(jí)異常的相關(guān)問(wèn)題,本文給大家分享解決方案,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06Android實(shí)現(xiàn)一個(gè)比相冊(cè)更高大上的左右滑動(dòng)特效(附源碼)
這篇文章主要介紹了Android實(shí)現(xiàn)一個(gè)比相冊(cè)更高大上的左右滑動(dòng)特效(附源碼),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02Android中封裝RecyclerView實(shí)現(xiàn)添加頭部和底部示例代碼
這篇文章主要給大家介紹了關(guān)于Android中封裝RecyclerView實(shí)現(xiàn)添加頭部和底部的相關(guān)資料,網(wǎng)上這方面的資料很多,但都不是自己需要的,索性自己寫一個(gè)分享出來(lái)供大家參考學(xué)習(xí),需要的朋友們下面隨著小編一起來(lái)學(xué)習(xí)學(xué)習(xí)吧。2017-08-08