首页 > 代码库 > Cocos2d-x 3.0多线程异步资源加载
Cocos2d-x 3.0多线程异步资源加载
Cocos2d-x从2.x版本到现在的Cocos2d-x 3.0 Final版,其引擎驱动核心依旧是一个单线程的“死循环”,一旦某一帧遇到了“大活儿”,比如Size很大的纹理资源加载或网络IO或大量计算,画面将 不可避免出现卡顿以及响应迟缓的现象。从古老的Win32 GUI编程那时起,Guru们就告诉我们:别阻塞主线程(UI线程),让Worker线程去做那些“大活儿”吧。
1 // AppDelegate.cpp 2 bool AppDelegate::applicationDidFinishLaunching() { 3 … … 4 FlashScene* scene = FlashScene::create(); 5 pDirector->runWithScene(scene); 6 7 return true; 8 } <span style="font-family:‘sans serif‘, tahoma, verdana, helvetica;line-height:1.5;"> </span>
在FlashScene init时,我们创建一个Resource Load Thread,我们用一个ResourceLoadIndicator作为渲染线程与Worker线程之间交互的媒介。
1 //FlashScene.h 2 3 struct ResourceLoadIndicator { 4 pthread_mutex_t mutex; 5 bool load_done; 6 void *context; 7 }; 8 9 class FlashScene : public Scene 10 { 11 public: 12 FlashScene(void); 13 ~FlashScene(void); 14 15 virtual bool init(); 16 17 CREATE_FUNC(FlashScene); 18 bool getResourceLoadIndicator(); 19 void setResourceLoadIndicator(bool flag); 20 21 private: 22 void updateScene(float dt); 23 24 private: 25 ResourceLoadIndicator rli; 26 }; 27 28 // FlashScene.cpp 29 bool FlashScene::init() 30 { 31 bool bRet = false; 32 do { 33 CC_BREAK_IF(!CCScene::init()); 34 Size winSize = Director::getInstance()->getWinSize(); 35 36 //FlashScene自己的资源只能同步加载了 37 Sprite *bg = Sprite::create("FlashSceenBg.png"); 38 CC_BREAK_IF(!bg); 39 bg->setPosition(ccp(winSize.width/2, winSize.height/2)); 40 this->addChild(bg, 0); 41 42 this->schedule(schedule_selector(FlashScene::updateScene) 43 , 0.01f); 44 45 //start the resource loading thread 46 rli.load_done = false; 47 rli.context = (void*)this; 48 pthread_mutex_init(&rli.mutex, NULL); 49 pthread_attr_t attr; 50 pthread_attr_init(&attr); 51 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 52 pthread_t thread; 53 pthread_create(&thread, &attr, 54 resource_load_thread_entry, &rli); 55 56 bRet=true; 57 } while(0); 58 59 return bRet; 60 } 61 62 static void* resource_load_thread_entry(void* param) 63 { 64 AppDelegate *app = (AppDelegate*)Application::getInstance(); 65 ResourceLoadIndicator *rli = (ResourceLoadIndicator*)param; 66 FlashScene *scene = (FlashScene*)rli->context; 67 68 //load music effect resource 69 … … 70 71 //init from config files 72 … … 73 74 //load images data in worker thread 75 SpriteFrameCache::getInstance()->addSpriteFramesWithFile( 76 "All-Sprites.plist"); 77 … … 78 79 //set loading done 80 scene->setResourceLoadIndicator(true); 81 return NULL; 82 } 83 84 bool FlashScene::getResourceLoadIndicator() 85 { 86 bool flag; 87 pthread_mutex_lock(&rli.mutex); 88 flag = rli.load_done; 89 pthread_mutex_unlock(&rli.mutex); 90 return flag; 91 } 92 93 void FlashScene::setResourceLoadIndicator(bool flag) 94 { 95 pthread_mutex_lock(&rli.mutex); 96 rli.load_done = flag; 97 pthread_mutex_unlock(&rli.mutex); 98 return; 99 }
我们在定时器回调函数中对indicator标志位进行检查,当发现加载ok后,切换到接下来的游戏开始场景:
1 void FlashScene::updateScene(float dt) 2 { 3 if (getResourceLoadIndicator()) { 4 Director::getInstance()->replaceScene( 5 WelcomeScene::create()); 6 } 7 }
通过monitor分析游戏的运行日志,我们看到了如下一些异常日志:
1 threadid=24: thread exiting, not yet detached (count=0) 2 threadid=24: thread exiting, not yet detached (count=1) 3 threadid=24: native thread exited without detaching
很是奇怪啊,我们在创建线程时,明明设置了 PTHREAD_CREATE_DETACHED属性了啊:
1 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
SimpleAudioEngine和UserDefault能有什么共同点呢?Jni调用。没错,这两个接口底层要适配多个平台,而对于Android 平台,他们都用到了Jni提供的接口去调用Java中的方法。而Jni对多线程是有约束的。Android开发者官网上有这么一段话:
1 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相关操作。
1 #if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) 2 #include "platform/android/jni/JniHelper.h" 3 #include <jni.h> 4 #endif 5 6 static void* resource_load_thread_entry(void* param) 7 { 8 … … 9 10 JavaVM *vm; 11 JNIEnv *env; 12 vm = JniHelper::getJavaVM(); 13 14 JavaVMAttachArgs thread_args; 15 16 thread_args.name = "Resource Load"; 17 thread_args.version = JNI_VERSION_1_4; 18 thread_args.group = NULL; 19 20 vm->AttachCurrentThread(&env, &thread_args); 21 … … 22 //Your Jni Calls 23 … … 24 25 vm->DetachCurrentThread(); 26 … … 27 return NULL; 28 }
关于什么是JavaVM,什么是JniEnv,Android Developer官方文档中是这样描述的:
1 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.2 The JNIEnv provides most of the JNI functions. Your native functions all receive a JNIEnv as the first argument.3 The JNIEnv is used for thread-local storage. For this reason, you cannot share a JNIEnv between threads.
上面的代码成功解决了线程崩溃的问题,但问题还没完,因为接下来我们又遇到了“黑屏”事件。所谓的“黑屏”,其实并不是全黑。但进入游戏 WelcomScene时,只有Scene中的LabelTTF实例能显示出来,其余Sprite都无法显示。显然肯定与我们在Worker线程加载纹理 资源有关了:
1 SpriteFrameCache::getInstance()->addSpriteFramesWithFile("All-Sprites.plist");
我们通过碎图压缩到一张大纹理的方式建立SpriteFrame,这是Cocos2d-x推荐的优化手段。但要想找到这个问题的根源,还得看monitor日志。我们的确发现了一些异常日志:
1 libEGL: call to OpenGL ES API with no current context (logged once per thread)
TextureCache::addImageAsync只是在worker线程进行了image数据的加载,而纹理对象Texture2D instance则是在addImageAsyncCallBack中创建的。也就是说纹理还是在Renderer线程中创建的,因此不会出现我们上面的 “黑屏”问题。模仿addImageAsync,我们来修改一下代码:
1 static void* resource_load_thread_entry(void* param) 2 { 3 … … 4 allSpritesImage = new Image(); 5 allSpritesImage->initWithImageFile("All-Sprites.png"); 6 … … 7 } 8 9 void FlashScene::updateScene(float dt) 10 { 11 if (getResourceLoadIndicator()) { 12 // construct texture with preloaded images 13 Texture2D *allSpritesTexture = TextureCache::getInstance()-> 14 addImage(allSpritesImage, "All-Sprites.png"); 15 allSpritesImage->release(); 16 SpriteFrameCache::getInstance()->addSpriteFramesWithFile( 17 "All-Sprites.plist", allSpritesTexture); 18 19 Director::getInstance()->replaceScene(WelcomeScene::create()); 20 } 21 }
原文链接:http://www.cocoachina.com/gamedev/cocos/2014/0429/8259.html