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到記憶體中的資料即可,無需再行載入。
一、為遊戲添加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(
"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一下試試吧。
二、解決崩潰問題
在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中執行的任務。在My Code裡,我利用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.
三、解決黑屏問題
上面的代碼成功解決了線程崩潰的問題,但問題還沒完,因為接下來我們又遇到了“黑屏”事件。所謂的“黑屏”,其實並不是全黑。但進入遊戲 WelcomScene時,只有Scene中的LabelTTF執行個體能顯示出來,其餘Sprite都無法顯示。顯然肯定與我們在Worker線程載入紋理 資源有關了:
複製代碼 代碼如下:
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("All-Sprites.plist");
我們通過碎圖壓縮到一張大紋理的方式建立SpriteFrame,這是Cocos2d-x推薦的最佳化手段。但要想找到這個問題的根源,還得看monitor日誌。我們的確發現了一些異常日誌:
複製代碼 代碼如下:
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());
}
}
完成這一修改後,遊戲畫面就變得一切正常了,多線程資源載入機制正式生效。