Cocos2d-x2.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画面:
  1. // AppDelegate.cpp
  2. bool AppDelegate::applicationDidFinishLaunching() {
  3. … …
  4. FlashScene* scene = FlashScene::create();
  5. pDirector->runWithScene(scene);
  6. return true;
  7. }
 
在FlashScene init时,我们创建一个Resource Load Thread,我们用一个ResourceLoadIndicator作为渲染线程与Worker线程之间交互的媒介。
  1. //FlashScene.h
  2. struct ResourceLoadIndicator {
  3. pthread_mutex_t mutex;
  4. bool load_done;
  5. void *context;
  6. };
  7. class FlashScene : public Scene
  8. {
  9. public:
  10. FlashScene(void);
  11. ~FlashScene(void);
  12. virtual bool init();
  13. CREATE_FUNC(FlashScene);
  14. bool getResourceLoadIndicator();
  15. void setResourceLoadIndicator(bool flag);
  16. private:
  17. void updateScene(float dt);
  18. private:
  19. ResourceLoadIndicator rli;
  20. };
  21. // FlashScene.cpp
  22. bool FlashScene::init()
  23. {
  24. bool bRet = false;
  25. do {
  26. CC_BREAK_IF(!CCScene::init());
  27. Size winSize = Director::getInstance()->getWinSize();
  28. //FlashScene自己的资源仅仅能同步载入了
  29. Sprite *bg = Sprite::create("FlashSceenBg.png");
  30. CC_BREAK_IF(!bg);
  31. bg->setPosition(ccp(winSize.width/2, winSize.height/2));
  32. this->addChild(bg, 0);
  33. this->schedule(schedule_selector(FlashScene::updateScene)
  34. , 0.01f);
  35. //start the resource loading thread
  36. rli.load_done = false;
  37. rli.context = (void*)this;
  38. pthread_mutex_init(&rli.mutex, NULL);
  39. pthread_attr_t attr;
  40. pthread_attr_init(&attr);
  41. pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
  42. pthread_t thread;
  43. pthread_create(&thread, &attr,
  44. resource_load_thread_entry, &rli);
  45. bRet=true;
  46. } while(0);
  47. return bRet;
  48. }
  49. static void* resource_load_thread_entry(void* param)
  50. {
  51. AppDelegate *app = (AppDelegate*)Application::getInstance();
  52. ResourceLoadIndicator *rli = (ResourceLoadIndicator*)param;
  53. FlashScene *scene = (FlashScene*)rli->context;
  54. //load music effect resource
  55. … …
  56. //init from config files
  57. … …
  58. //load images data in worker thread
  59. SpriteFrameCache::getInstance()->addSpriteFramesWithFile(
  60. "All-Sprites.plist");
  61. … …
  62. //set loading done
  63. scene->setResourceLoadIndicator(true);
  64. return NULL;
  65. }
  66. bool FlashScene::getResourceLoadIndicator()
  67. {
  68. bool flag;
  69. pthread_mutex_lock(&rli.mutex);
  70. flag = rli.load_done;
  71. pthread_mutex_unlock(&rli.mutex);
  72. return flag;
  73. }
  74. void FlashScene::setResourceLoadIndicator(bool flag)
  75. {
  76. pthread_mutex_lock(&rli.mutex);
  77. rli.load_done = flag;
  78. pthread_mutex_unlock(&rli.mutex);
  79. return;
  80. }
 
我们在定时器回调函数中对indicator标志位进行检查。当发现载入ok后,切换到接下来的游戏開始场景: 
  1. void FlashScene::updateScene(float dt)
  2. {
  3. if (getResourceLoadIndicator()) {
  4. Director::getInstance()->replaceScene(
  5. WelcomeScene::create());
  6. }
  7. }
 
到此。FlashScene的初始设计和实现完毕了。Run一下试试吧。
 
二、崩溃
GenyMotion的4.4.2模拟器上,游戏执行的结果并没有如我期望,FlashScreen显现后游戏就异常崩溃退出了。

 
通过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);
 
怎么还会出现这个问题,并且竟然有三条日志。

翻看了一下引擎内核的代码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相关操作。

  1. #if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
  2. #include "platform/android/jni/JniHelper.h"
  3. #include <jni.h>
  4. #endif
  5. static void* resource_load_thread_entry(void* param)
  6. {
  7. … …
  8. JavaVM *vm;
  9. JNIEnv *env;
  10. vm = JniHelper::getJavaVM();
  11. JavaVMAttachArgs thread_args;
  12. thread_args.name = "Resource Load";
  13. thread_args.version = JNI_VERSION_1_4;
  14. thread_args.group = NULL;
  15. vm->AttachCurrentThread(&env, &thread_args);
  16. … …
  17. //Your Jni Calls
  18. … …
  19. vm->DetachCurrentThread();
  20. … …
  21. return NULL;
  22. }
 
关于什么是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线程载入纹理 资源有关了: 
  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)
 
通过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,我们来改动一下代码:
  1. static void* resource_load_thread_entry(void* param)
  2. {
  3. … …
  4. allSpritesImage = new Image();
  5. allSpritesImage->initWithImageFile("All-Sprites.png");
  6. … …
  7. }
  8. void FlashScene::updateScene(float dt)
  9. {
  10. if (getResourceLoadIndicator()) {
  11. // construct texture with preloaded images
  12. Texture2D *allSpritesTexture = TextureCache::getInstance()->
  13. addImage(allSpritesImage, "All-Sprites.png");
  14. allSpritesImage->release();
  15. SpriteFrameCache::getInstance()->addSpriteFramesWithFile(
  16. "All-Sprites.plist", allSpritesTexture);
  17. Director::getInstance()->replaceScene(WelcomeScene::create());
  18. }
  19. }
 
完毕这一改动后。游戏画面就变得一切正常了,多线程资源载入机制正式生效。

Cocos2d-x 3.0多线程异步资源载入的更多相关文章

  1. Cocos2d-x 3.0多线程异步资源载入代码

    // AppDelegate.cpp bool AppDelegate::applicationDidFinishLaunching() { - - FlashScene* scene = Flash ...

  2. [原]unity3d之http多线程异步资源下载

    郑重声明:转载请注明出处 U_探索 本文诞生于乐元素面试过程,被面试官问到AssetBundle多线程异步下载时,愣了半天,同样也被深深的鄙视一回(做了3年多u3d 这个都没用过),所以发誓要实现出来 ...

  3. 最新版本号cocos2d&#173;2.0&#173;x&#173;2.0.2使用新资源载入策略!不再沿用-hd、-

     前段时间cocos2dx更新了最新版本号cocos2d­2.0­x­2.0.2.也从这个版本号開始对于资源载入与管理都改变了策略. 在之前的载入方式都是通过沿用与cocos2d-iphone一样 ...

  4. 关于Cocos2d-x多线程异步载入资源的问题

    我们通常使用CCSpriteFrameCache::sharedSpriteFrameCache()->addSpriteFramesWithFile("xxx.plist" ...

  5. 可扩展多线程异步Socket服务器框架EMTASS 2.0 (转自:http://blog.csdn.net/hulihui)

    可扩展多线程异步Socket服务器框架EMTASS 2.0 (转自:http://blog.csdn.net/hulihui) 0 前言 >>[前言].[第1节].[第2节].[第3节]. ...

  6. C# 实现的多线程异步Socket数据包接收器框架

    转载自Csdn : http://blog.csdn.net/jubao_liang/article/details/4005438 几天前在博问中看到一个C# Socket问题,就想到笔者2004年 ...

  7. Servlet3.0对异步处理的支持

    Servlet工作流程 Servlet 3.0 之前,一个普通 Servlet 的主要工作流程大致如下: Servlet 接收到请求之后,可能需要对请求携带的数据进行一些预处理: 调用业务接口的某些方 ...

  8. Redis 6.0 多线程重磅发布!!!

    Redis 6.0在5.2号这个美好的日子里悄无声息的发布了,这次发布在IT圈犹如一颗惊雷一般,因为这是redis最大的一次改版,首次加入了多线程. 作者Antirez在RC1版本发布时在他的博客写下 ...

  9. javascript异步延时载入及推断是否已载入js/css文件

    <html> <head> <script type="text/javascript"> /**======================= ...

随机推荐

  1. Flex AsDoc 完整版

    Flex 生成AsDoc用的是SDK自带的asdoc.exe工具 生成AsDoc文档的方式有两种:ant或者FlashBuilder 外部配置工具 方法一:外部配置工具 新增一个外部配置工具.过程例如 ...

  2. kettle 使用excel模板导出数据

    通过excel进行高速开发报表: 建设思路: 1.首先制订相关的execl模板. 2.通过etl工具(kettle)能够高速的 将数据库中的数据按excel模板导出成新的excel就可以. 当中ket ...

  3. 具体图解 Flume介绍、安装配置

    写在前面一: 本文总结"Hadoop生态系统"中的当中一员--Apache Flume 写在前面二: 所用软件说明: 一.什么是Apache Flume 官网:Flume is a ...

  4. VB.NET机房收费系统总结

    总感觉这次机房收费系统非常有份量,一直没有下手总结,从2014-7-27至2014-9-29.这中间有太多故事和成长.首先说一下两次机房收费系统的不同,它是我们从面向过程向面向对象的一个转变.在.NE ...

  5. Visual studio 编译时copy文件、文件夹

    项目属性--生成事件 添加命令 xcopy /y /d "$(SolutionDir)Dll\Linphone\*.dll" "$(TargetDir)\Linphone ...

  6. bzoj2806: [Ctsc2012]Cheat(SAM+DP)

    2806: [Ctsc2012]Cheat 题目:传送门 题解: 感觉这题考的更多的就是DP啊... 看完题目的第一反应就是广义SAM...(然而并不会) 再YY一会儿想起来可以直接将作文库连成一个母 ...

  7. mysql(8.0.*版本 windows10 )忘记密码解决方案

    安装完mysql-8.0.13-winx64后,一些列的安装命令过后再执行mysql -uroot -p之后 报错了 what fuck 什么鬼,就是这个错 ERROR (): Access deni ...

  8. FMDB条件更新

    更新操作返回一个BOOL值,YES表示操作成功,NO表示执行过程遇到错误,可以通过-lastErrorMessage和-lastErrorCode查看错误信息.使用executeUpdate:方法执行 ...

  9. datatable.rows.indexof(dr)返回的是啥?

    返回的是Int类型的 行索引值,从0开始.也就是说,第一行是0.最后一行就是rows.count - 1.不会返回-1."这是第" + OldDt.Rows.IndexOf(ite ...

  10. BAT三家互联网公司哪家更注重用户体验?

    这几天百度的用户体验又成了设计圈关注的对象,李彦宏好不容易刷出来的好感度一下子被打入了冰点,通过此次事件,不难看出现在的互联网用户对于产品的体验要求越来越高,作为一名美图秀秀级别选手,很难领悟“好设计 ...