COCOS2D-X中UI动画导致闪退与UI动画浅析

前两天和同事一起查一个游戏的闪退问题,log日志显示最后挂在CCNode* ActionNode::getActionNode()函数中的首行CCNode* cNode = dynamic_cast<CCNode*>(m_Object),由于不是必现bug,出现概率极低,单从代码来看,唯一的可能就是走到这里时m_Object已经为null了,所以才会挂出去。当然经过不懈努力,问题还是得以解决,这里mark一下,留作以后复习。

想方设法也无法重现的情况下,我们只能一步一步的分析UI动画的生命周期,借以希望发现问题所在,为此,我们特意从游戏UI中找了一个包含UI动画的界面,单独加载进行测试,UI动画很简单,时长两秒,循环播放,因为最后发现的闪退原因跟动画的内容无关,这里就不描述了。(补充下我们的引擎版本是Cocos2d-x2.2.2,CocosStudio1.6,VS2013),在后面我会结合Cocos2d-x3.5说一下触控的改进。

我把大概流程弄了一个图,看起来直观一点,为了节俭空间,我只列出需要的关键代码描述下流程:

(由于CocosStudio目录下的actions里面的ActionManager、ActionNode等类名跟Cocos2d底层的actions目录下的类名是一样的,下文中没有特别说明的就是指CocosStudio下的类)

数据流向链:

上图的顺序就是各个类的调用顺序,从上到下。其实这上面的所有内容都是围绕着json文件的解析来进行的,如果接着纵向往下走的话,还有纹理的解析存储等。

对于使用者来讲,我们只关心1和3所抛出来的接口。在详细了解内部机制之前,先看下各个类之间的关系(从ActionManager开始):

结合最上面的图,可以看出来,引擎在处理UI动画时其实可以算作是两条线单独走的,UI界面作为动画的承载方,在各个类中以Widget、rootWidget、root等名称跟随者动画的数据流向,一直到ActionNode结束,而ActionManager作为动画的具体管理方,是单独的一套流程,所以UIWidget和UIAction之间只是一种弱关联状态。由此可以看出,UI动画的最小执行节点其实就是ActionNode,多个ActionNode执行不同的动作,组成一个UI动画。同时呢,可以看出,这个由GUIReader创建出来的UIWidget一直都是作为动画或者动作的承载主体贯穿了整个数据链。

执行链:(同样,只列出关键代码)

从上图可以看出,一个UI动画的播放流程跟它的解析加载流向是一样的,最终都会走到ActionNode这一层,但是,注意看ActionObject这里,在ActionObject的play函数里面,有何定时器操作,也就是说,UI动画的更新循环操作是在这里进入的,我们进去看下这个回调的具体实现:


void ActionObject::simulationActionUpdate(float dt)

{

bool isEnd = true;

int nodeNum = m_ActionNodeList->count();

for ( int i = 0; i < nodeNum; i++ )

{

ActionNode* actionNode = (ActionNode*)m_ActionNodeList->objectAtIndex(i);

if (actionNode->isActionDoneOnce() == false)

{

isEnd = false;

break;

}

}

if (isEnd)

{

if (m_CallBack != NULL)

{

m_CallBack->execute();

}

if (m_loop)

{

this->play();

}

}

}

这里就发现了,这个函数不仅跟随帧循环模拟出一个动画的循环,里面还要不停的去for循环判断某个节点动画是否播放完了,是否要重复播放,毕竟,每个节点动画的播放时长可能是不一样的,而对于整个动画对象ActionObject来说是通过跟随帧循环递归来实现的。

--------分割一下,感觉有点乱了-----------------------------------------------

从上面的分析来看,这个流程还是比较清晰的,但是最开始说的闪退问题是出现在哪里呢,我们回过头去看一下UI动画对象,也就是ActionObject这个类的play函数,刚刚上面说过,这个play是通过调用ActionNode的play函数来实现动画播放和循环的,但是仔细分析这个函数,发现在每次动画播放之前,都会调用stop函数,这就有点费解了,万一动画还没播完怎么办,我们先看下ActionObject的play函数具体实现:


void ActionObject::play()

{

stop();

this->updateToFrameByTime(0.0f);//这个函数的作用是更新纹理,这里不作深究

int frameNum = m_ActionNodeList->count();

for ( int i = 0; i < frameNum; i++ )

{

ActionNode* actionNode = (ActionNode*)m_ActionNodeList->objectAtIndex(i);

actionNode->playAction(); //这里往下走就会调入CCNode的runAction函数里面,就不做深究了

}

if (m_loop)

{

m_pScheduler->scheduleSelector(schedule_selector(ActionObject::simulationActionUpdate), this, 0.0f , kCCRepeatForever, 0.0f, false);

}

else

{

m_pScheduler->scheduleSelector(schedule_selector(ActionObject::simulationActionUpdate), this, 0.0f, false);

}

}

这个函数很简单,先调用stop函数,然后更新纹理帧,然后又是一个for循环,挨个去播放节点动作,要是一个动画节点过多,会不会掉帧卡屏,哈哈,这是极有可能的。这个stop有点费解,看下它的具体实现在做分析:


void ActionObject::stop()

{

int frameNum = m_ActionNodeList->count();

for ( int i = 0; i < frameNum; i++ )

{

ActionNode* actionNode = (ActionNode*)m_ActionNodeList->objectAtIndex(i);

actionNode->stopAction();

}

m_pScheduler->unscheduleSelector(schedule_selector(ActionObject::simulationActionUpdate), this);

m_bPause = false;

}

在这里面,又调用到了ActionNode的stopAction函数,跟进去看了下,最后走到了cocos2d的底层ActionManager的removeAction这个函数里面,发现一个很有意思的事情,看看这个函数的实现:


void CCActionManager::removeAction(CCAction *pAction)

{

if (pAction == NULL)

{

return;

}

tHashElement *pElement = NULL;

CCObject *pTarget = pAction->getOriginalTarget();

HASH_FIND_INT(m_pTargets, &pTarget, pElement);

if (pElement)

{

unsigned int i = ccArrayGetIndexOfObject(pElement->actions, pAction);

if (UINT_MAX != i)

{

removeActionAtIndex(i, pElement);

}

}

else

{

CCLOG("cocos2d: removeAction: Target not found: %s", pAction->description());

}

}

注意最后的打印,这个狗血的东西总是出现在游戏的日志中,也不影响游戏的运行,官方没有给出为什么,只是说不影响。到这里,其实发现也没什么不妥之处啊,好吧,只能让程序跑起来,断点跟进去了,当然,这也是一个技巧性的东西,因为,这里动画的播放是跟随帧循环的,断点也不好弄,要是断在帧循环函数内部了,那基本上也看不出什么来,要么问题一大堆,要么一点问题都没有,所以,最好的办法就是上打印这个神器,把各个关键点的内容打印出来,果然,还是有效果的。

接下来,就是重点了,在加上打印之后发现,当这个包含UI动画的窗口关闭之后,动画内部的这个simulationActionUpdate函数居然没有停下来,还在跟着帧循环死命跑着,整个UI界面都已经关闭并且释放掉了,这个居然没停,很明显问题出在这里了,仔细分析simulationActionUpdate函数实现(上面已经贴出来,可以翻看下)之后发现,这里有个临时变量isEnd,在for循环遍历判断ActionNode的时候,如果每次这个变量都无法赋值成true;那么这个查询行为就停不下来了。进一步看下这个isEnd变量的赋值条件,由一个函数决定


if (actionNode->isActionDoneOnce() == false)

{

isEnd = false;

break;

}

分析到这里,基本上动画的播放和停止也就理清楚了,在这个simulationActionUpdate函数里面,首先,如果是循环动画,那么,当动画没有播放完成时,这个isEnd是false,但是如果这个isEnd一直处在false状态(动画一直处在没有播放完成的状态)时,那么悲剧就来了,这个循环就一直在查询动画状态,而没有办法进行下一次的播放。那么这个isEnd为什么不正常了呢,跟着isActionDoneOnce这个函数继续往下走,发现在走到了CCRepeat里面的idDone函数中:


bool CCRepeat::isDone(void)

{

return m_uTotal == m_uTimes;

}

不用问,函数里面的两个变量一个是动画当前播放时间,一个事动画总时间,简单粗暴的判断标准,要想isEnd始终是false,那么就要isDone始终返回false,就是说两个时间不相等,这就很简单了,在UI窗口正常运行时,将UI窗口关掉就行了,因为关闭UI窗口的那一刹那刚好是动画执行完成的那一刹那的几率想想也是很低的,这样子,窗口关闭了,动画也没有继续执行,这个播放时间就定格在那一刹那,随着帧循环,上面那个狗血的log就出现了。

那么问题来了,之前说的闪退的情况是怎么出现的呢,很简单,那就是中的戳中了那一刹那,关闭窗口和动画执行完毕在同一时间,那么,isEnd就是true了,这样子,就会马上执行下一次的play,在这次play中会先执行一次stop函数,好了,主角终于来了,stop函数会调用ActionNode的stopAction函数,看下源代码:


void ActionNode::stopAction()

{

CCNode* cNode = this->getActionNode();

if (cNode != NULL && m_action != NULL)

{

cNode->stopAction(m_action);

}

}

在这里面,首先会调用getActionNode,再看源代码:


CCNode* ActionNode::getActionNode()

{

CCNode* cNode = dynamic_cast<CCNode*>(m_Object);

if (cNode != NULL)

{

return cNode;

}

else

{

//这几句是很狗血的,无用的代码,在新版本引擎里面已经删掉了

cocos2d::gui::Widget* rootWidget = dynamic_cast<cocos2d::gui::Widget*>(m_Object);

if (rootWidget != NULL)

{

return rootWidget;

}

}

return NULL;

}

好吧,之前已经说过了,窗口已经关掉了,那么这个m_Object肯定已经是空的(这个m_Object就是最开始的rootWidget,前面说过的它跟随着json的解析一路保存了引用在各个类里面的),这样子,就挂了。这也就说明了为什么这个闪退现象很难重现,毕竟卡时间卡的这么准是一件很难的事情。

那么,总结一下,为什么会出现这种现象呢,首先吐槽下引擎的架构逻辑,然后就是我们自己的代码不够严谨了,其实只要在关闭窗口时,首先将动画停掉,就不会出现这种事情了。淡然在做这个工作的时候,又发现一个更加蛋疼的事情,CocosStudio这里的ActionManager单例的release函数是这样子写的:


void ActionManager::releaseActions()

{

m_pActionDic->removeAllObjects();

}

这怎么可以呢,为了省事,我翻了下3.5版本的引擎,发现这个函数已经重新写过了,直接扒过来用:


void ActionManagerEx::releaseActions()

{

std::unordered_map<std::string, cocos2d::Vector<ActionObject*>>::iterator iter;

for (iter = _actionDic.begin(); iter != _actionDic.end(); iter++)

{

cocos2d::Vector<ActionObject*> objList = iter->second;

ssize_t listCount = objList.size();

for (ssize_t i = 0; i < listCount; i++) {

ActionObject* action = objList.at(i);

if (action != nullptr) {

action->stop();

}

}

objList.clear();

}

_actionDic.clear();

}

照着2.2.2版本的样式改吧改吧就成了。

好叻,mark完毕,有什么没说清楚或者整错了的地方,希望有看到的兄弟姐妹们指出来,共同学习。

时间: 2024-10-14 09:11:53

COCOS2D-X中UI动画导致闪退与UI动画浅析的相关文章

windows10下autocad出现致命错误导致闪退怎么办

在windows10系统下,使用autocad2014复制文件时,偶尔会遇到AutoCAD错误中断提示"致命错误:Unhandled Access Violation Reading 0x0040 Exception at 3fd17cb1h",最终导致闪退,编辑的文件丢失等.经过仔细排查,发现是AUTOCAD版本过低引起,通过下载最新补丁,可以解决此问题.下面,小编就为大家分享下具体步骤. 推荐:win10 64位旗舰版官网下载 具体方法如下: 1.发生错误后点如图点击确定: 2.如

直接双击启动tomcat中的startup.bat闪退原因及解决方法

免安装的tomcat双击startup.bat后,启动窗口一闪而过,而且tomcat服务未启动. 原因是:在启动tomcat是,需要读取环境变量和配置信息,缺少了这些信息,就不能登记环境变量,导致了tomcat的闪退. 解决办法: 1.在已解压的tomcat的bin文件夹下找到startup.bat,右击->编辑.在文件头加入下面两行: SET JAVA_HOME=D:\Java\jdk1.6.0_10 (java jdk目录) SET TOMCAT_HOME=E:\tomcat-6.0.35

iOS 10 因苹果健康导致闪退 crash-b

如果在app中调用了苹果健康,iOS10中会出现闪退.控制台报出的原因是: Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'NSHealthUpdateUsageDescription must be set in the app's Info.plist in order to request write authorization.' 这是因为我们要在info.plist文件中声

iOS 10 因苹果健康导致闪退 crash

如果在app中调用了苹果健康,iOS10中会出现闪退.控制台报出的原因是: Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'NSHealthUpdateUsageDescription must be set in the app's Info.plist in order to request write authorization.' 这是因为我们要在info.plist文件中声

Cocos2d-x操作:文件读取导致闪退

问题1:fopen 在vs下使用fopen进行文件处理,跑通了,但是移植到android下时就出现了一大推问题,首先需要理解的是在vs下开发资源是存放在执行文件的相同目录下的,而移植到android下时资源是存放在assets目录下的,之前尝试过使用以下的方法: char* fileName = "bg.png";  string filepath = [/font][/color]FileUtils::getInstance()[font=Arial]->fullPathFor

ThreadPool.QueueUserWorkItem引发的血案,线程池异步非正确姿势导致程序闪退的问题

ThreadPool是.net System.Threading命名空间下的线程池对象.使用QueueUserWorkItem实现对异步委托的先进先出有序的回调.如果在回调的方法里面发生异常则应用程序会出现闪退.当然是指不处理那个异常的情况下.这不公司的CMS在生产环境频频出现闪退的情况.该死的是,原来用老机器配置不高的情况下没有出现过.换了更好的新机器后出现的. // // 摘要: // 将方法排入队列以便执行,并指定包含该方法所用数据的对象.此方法在有线程池线程变得可用时执行. // //

阿里客户端工程师试题简析——Android应用的闪退(crash)分析

1. 问题描述 闪退(Crash)是客户端程序在运行时遭遇无法处理的异常或错误时而退出应用程序的表现,请从crash发生的原因分类与解决方法.在出现crash后如何捕捉并分析异常这两个问题给出自己的解决方案. 我们以Android平台为例,介绍下如何捕获Android应用的闪退信息,以帮助我们定位和解决导致闪退的问题代码. 2. Android中的闪退 在讲解Android中的闪退之前,我们先来简单的复习下Java中的异常. (1)Java中的异常 Java中的异常层次结构如下图所示: 我们可以

阿里安卓面试分析: Android应用的闪退(crash)问题跟踪和解析

一:问题描述    闪退(Crash)是客户端程序在运行时遭遇无法处理的异常或错误时而退出应用程序的表现,请从crash发生的原因分类与解决方法.在出现crash后如何捕捉并分析异常这两个问题给出自己的解决方案.    我们以Android平台为例,介绍下如何捕获Android应用的闪退信息,以帮助我们定位和解决导致闪退的问题代码.二:Android中的闪退    在讲解Android中的闪退之前,我们先来简单的复习下Java中的异常.1.Java中的异常    Java中的异常层次结构如下图所

Tomcat start.bat闪退:JRE_HOME环境变量配置不对

最近在配置Tomcat上遇到startup.bat启动闪退,在网上找了很多方法,都没解决.后来在网上找到两种问题,更改了两次,解决了.现将我遇到的问题分享,希望对遇到同样问题有帮助. 1.很多初学者对jdk的配置不明确,不知道jdk与jre是做什么的,配置文件的时候,难免将jdk与jre配置混淆. 建议卸载jdk后,在F盘新建jdk文件,在里面新建jdk.jre文件,便于区分. 2.安装jdk,第一个路径:jdk路径(如果默认路径是自己建立的路径,则不需更改) 第二个路径:jre路径(如果默认路