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