前两天和同事一起查一个游戏的闪退问题,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完毕,有什么没说清楚或者整错了的地方,希望有看到的兄弟姐妹们指出来,共同学习。

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

  1. 关于ArcMap中打开ArcToolbox导致闪退的解决办法

    最近好久不用ArcGis的小编要用到ArcMap去发送一个GP服务,发现按照套路打开ArcMap点击ArcToolbox时,发生了ArcMap的闪退现象,几经周折终于解决了问题. 希望也遇到这类问题的 ...

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

    免安装的tomcat双击startup.bat后,启动窗口一闪而过,而且tomcat服务未启动. 原因是:在启动tomcat是,需要读取环境变量和配置信息,缺少了这些信息,就不能登记环境变量,导致了t ...

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

    如果在app中调用了苹果健康,iOS10中会出现闪退.控制台报出的原因是: Terminating app due to uncaught exception 'NSInvalidArgumentEx ...

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

    如果在app中调用了苹果健康,iOS10中会出现闪退.控制台报出的原因是: Terminating app due to uncaught exception 'NSInvalidArgumentEx ...

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

    免安装的tomcat双击startup.bat后,启动窗口一闪而过,而且tomcat服务未启动. 原因是:在启动tomcat是,需要读取环境变量和配置信息,缺少了这些信息,就不能登记环境变量,导致了t ...

  6. Windows10中打开git bash闪退解决方案

    重装系统后打开gitbash莫名其妙闪退... 究其原因,好像是盗版系统的null.sys文件损坏 那就在这里附上null.sys文件的下载链接: https://pan.baidu.com/s/1V ...

  7. android内嵌入webview导致闪退

    这里碰到的是各种闪退情况之一,webview退出后,程序里立马需要申请内存空间做别的事情,这时内存不够就会闪退,做法就是延时个几百毫秒,在这段时间内让java把该回收的内存都回收,然后延时到了再做接下 ...

  8. 针对tomcat中startup启动服务器闪退的情况

    1.要保证你配置jdk环境变量无误:java环境变量配置详解. 2. 3.在环境变量中设置CATALINA_HOME:

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

    ThreadPool是.net System.Threading命名空间下的线程池对象.使用QueueUserWorkItem实现对异步委托的先进先出有序的回调.如果在回调的方法里面发生异常则应用程序 ...

随机推荐

  1. VS2010里, using System.Data.OracleClient; 不可用

    当我试图去引用System.Data.OracleClient 这个命名空间时,VS 显示不存在 但是在对象浏览器里却可以找到这个命名空间及里边的对象 另外好像也没有区分清楚 using 和Refer ...

  2. Sqlite日期类型问题:该字符串未被识别为有效的 DateTime(String not recognized as a valid datetime)

    使用SQLite抛出异常: 该字符串未被识别为有效的 DateTime 错误(String not recognized as a valid datetime) 解决方法: 也可以在连接字符串 修改 ...

  3. C#在数据层过滤属性中的主键

    C#使用泛型+反射做为数据层时,一个很都头疼的问题,如何让C#属性在程序里识别出哪个属性是主键,在拼接SQL时,不能把主键拼接到SQL语句里. 这个需要自定义一个属性.新建一个类文件,命名为Prosp ...

  4. HTML5 <video> - 使用 DOM 进行控制

    HTML5 <video> 元素同样拥有方法.属性和事件. 其中的方法用于播放.暂停以及加载等.其中的属性(比如时长.音量等)可以被读取或设置.其中的 DOM 事件能够通知您,比方说,&l ...

  5. 程序测试用的IE浏览器第二次无法加载入口程序的问题及其解决方法

    注:针对的是C#程序(Silverlight) 第一步.找到入口程序所在的路径,以记事本形式打开<入口程序.csproj>,由于之前配置入口程序时,设置了“Use Local IIS We ...

  6. Alpha版使用说明书

     游戏规则:             玩家是黑色的小煤球哦!             通过方向键或者ASDW,来控制小球移动(上.下.左.右).             累计时间,直到碰到了红色的小球 ...

  7. 曲线参数化的Javascript实现(理论篇)

    在关键帧动画的制作过程中,动画师在k物体运动的过程中,一般要确定2个参数: 1)运动轨迹(表示物体运动的路径): 2)速度曲线(表示物体随时间的速度变化). 对于运动轨迹通常选用一定的样条曲线,通过动 ...

  8. 一些初级Java错误,不定期增加

    1. Error: Dangling meta character '*' near index 0 对字符串使用split()方法截取 * ? + / | 等字符的时候会报以下异常 Dangling ...

  9. C++ Daily 《6》---- 类静态对象与函数静态对象

    C++ 的一个哲学基础是,你不应该为你使用的东西付出代价. class 拥有一个 static 成员,即使从未被用到,它也会被构造和析构: 而 函数拥有一个 static 成员, 如果这个函数从未被调 ...

  10. 直接请求json文件爬取天眼查企业信息(未解决验证码问题)——python3实现

    几个月前...省略一堆剧情...直接请求json文件爬取企业信息未成功,在知乎提问后,得到解决,有大佬说带上全部headers和cookie是可以的,我就又去试了下,果然可以(之前自己试的时候不行,没 ...