程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> COCOS2D-X中UI動畫導致閃退與UI動畫淺析,cocos2d-xui

COCOS2D-X中UI動畫導致閃退與UI動畫淺析,cocos2d-xui

編輯:C++入門知識

COCOS2D-X中UI動畫導致閃退與UI動畫淺析,cocos2d-xui


前兩天和同事一起查一個游戲的閃退問題,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完畢,有什麼沒說清楚或者整錯了的地方,希望有看到的兄弟姐妹們指出來,共同學習。

 

    

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved