上一章,我們分析Node類的源碼,在Node類里面耦合了一個 Scheduler 類的對象,這章我們就來剖析Cocos2d-x的調度器 Scheduler 類的源碼,從源碼中去了解它的實現與應用方法。
直入正題,我們打開CCScheduler.h文件看下里面都藏了些什么。
打開了CCScheduler.h 文件,還好,這個文件沒有ccnode.h那么大有上午行,不然真的吐血了, 僅僅不到500行代碼。這個文件里面一共有五個類的定義,老規矩,從加載的頭文件開始閱讀。
#include <functional> #include <mutex> #include <set> #include "CCRef.h" #include "CCVector.h" #include "uthash.h" NS_CC_BEGIN /** * @addtogroup global * @{ */ class Scheduler; typedef std::function<void(float)> ccSchedulerFunc;
代碼很簡單,看到加載了ref類,可以推斷Scheduler 可能也繼承了ref類,對象統一由Cocos2d-x內存管理器來管理。
這點代碼值得注意的就是下面 定義了一個函數類型 ccSchedulerFunc 接收一個float參數 返回void類型。
下面我們看這個文件里定義的第一個類 Timer
class CC_DLL Timer : public Ref { protected: Timer(); public: /** get interval in seconds */ inline float getInterval() const { return _interval; }; /** set interval in seconds */ inline void setInterval(float interval) { _interval = interval; }; void setupTimerWithInterval(float seconds, unsigned int repeat, float delay); virtual void trigger() = 0; virtual void cancel() = 0; /** triggers the timer */ void update(float dt); protected: Scheduler* _scheduler; // weak ref float _elapsed; bool _runForever; bool _useDelay; unsigned int _timesExecuted; unsigned int _repeat; //0 = once, 1 is 2 x executed float _delay; float _interval; };
第一點看過這個Timer類定義能了解到的信息如下:
- Timer類也是Ref類的子類,采用了cocos2d-x統一的內存管理機制。
- 這里一個抽象類。必須被繼承來使用。
- Timer主要的函數就是update,這個我們重點分析。
初步了解之后,我們按照老方法,先看看Timer類都有哪些成員變量,了解一下它的數據結構。
第一個變量為
Scheduler* _scheduler; // weak ref
這是一個Scheduler類的對象指針,后面有一個注釋說這個指針是一個 弱引用,弱引用的意思就是,在這個指針被賦值的時候並沒有增加對_scheduler的引用 計數。
后面幾個變量也很好理解。
float _elapsed; // 渡過的時間. bool _runForever; // 狀態變量,標記是否永遠的運行。 bool _useDelay; // 狀態變量,標記是否使用延遲 unsigned int _timesExecuted; // 記錄已經執行了多少次。 unsigned int _repeat; // 定義要執行的總次數,0為1次 1為2次 …… float _delay; // 延遲的時間 單位應該是秒 float _interval;// 時間間隔。
總結一下,通過分析Timer類的成員變量,我們可以知道這是一個用來描述一個計時器的類,
每隔 _interval 來觸發一次,
可以設置定時器觸發時的延遲 _useDelay和延遲時間 _delay.
可以設置定時器觸發的次數_repeat 也可以設置定時器永遠執行 _runforever
下面看Timer類的方法。
getInterval 與 setInterval不用多說了,就是_interval的 讀寫方法。
下面看一下 setupTimerWithInterval方法。
void Timer::setupTimerWithInterval(float seconds, unsigned int repeat, float delay) { _elapsed = -1; _interval = seconds; _delay = delay; _useDelay = (_delay > 0.0f) ? true : false; _repeat = repeat; _runForever = (_repeat == kRepeatForever) ? true : false; }
這也是一個設置定時器屬性的方法。
參數 seconds是設置了_interval
第二個參數repeat設置了重復的次數
第三個delay設置了延遲觸發的時間。
通過 這三個參數的設置還計算出了幾個狀態變量 根據 delay是否大於0.0f計算了_useDelay
#define kRepeatForever (UINT_MAX -1)
根據 repeat值是否是 kRepeatForever來設置了 _runforever。
注意一點 第一行代碼
_elapsed = -1;
這說明這個函數 setupTimerWithInterval 是一個初始化的函數,將已經渡過的時間初始化為-1。所以在已經運行的定時器使用這個函數的時候計時器會重新開始。
下面看一下重要的方法 update
void Timer::update(float dt)//參數dt表示距離上一次update調用的時間間隔,這也是從后面的代碼中分析出來的。 { if (_elapsed == -1)// 如果 _elapsed值為-1表示這個定時器是第一次進入到update方法 作了初始化操作。 { _elapsed = 0; _timesExecuted = 0; } else { if (_runForever && !_useDelay) {//standard timer usage _elapsed += dt; //累計渡過的時間。 if (_elapsed >= _interval) { trigger(); _elapsed = 0; //觸發后將_elapsed清除為0,小魚分析這里可能會有一小點的問題,因為 _elapsed值有可能大於_interval這里沒有做冗余處理,所以會吞掉一些時間,比如 1秒執行一次,而10秒內可能執行的次數小於10,吞掉多少與update調用的頻率有關系。 } } else {//advanced usage _elapsed += dt; if (_useDelay) { if( _elapsed >= _delay ) { trigger(); _elapsed = _elapsed - _delay;//延遲執行的計算,代碼寫的很干凈 _timesExecuted += 1; _useDelay = false;//延遲已經過了,清除_useDelay標記。 } } else { if (_elapsed >= _interval) { trigger(); _elapsed = 0; _timesExecuted += 1; } } if (!_runForever && _timesExecuted > _repeat)//觸發的次數已經滿足了_repeat的設置就取消定時器。 { //unschedule timer cancel(); } } } }
這個update 代碼很簡單,就是一個標准的定時器觸發邏輯,沒有接觸過的同學可以試模仿一下。
在這個update方法里,調用了 trigger與 cancel方法,現在我們可以理解這兩個抽象方法是個什么作用,
trigger是觸發函數
cancel是取消定時器
具體怎么觸發與怎么取消定時器,就要在Timer的子類里實現了。
Timer類源碼我們分析到這里,下面看Timer類的第一個子類 TimerTargetSelector 的定義
class CC_DLL TimerTargetSelector : public Timer { public: TimerTargetSelector(); /** Initializes a timer with a target, a selector and an interval in seconds, repeat in number of times to repeat, delay in seconds. */ bool initWithSelector(Scheduler* scheduler, SEL_SCHEDULE selector, Ref* target, float seconds, unsigned int repeat, float delay); inline SEL_SCHEDULE getSelector() const { return _selector; }; virtual void trigger() override; virtual void cancel() override; protected: Ref* _target; SEL_SCHEDULE _selector; };
這個類也很簡單。
我們先看一下成員變量 一共兩個成員變量
Ref* _target;
這里關聯了一個 Ref對象,應該是執行定時器的對象。
SEL_SCHEDULE _selector;
SEL_SCHEDULE 這里出現了一個新的類型,我們跟進一下,這個類型是在Ref類下面定義的,我們看一下。
class Node; typedef void (Ref::*SEL_CallFunc)(); typedef void (Ref::*SEL_CallFuncN)(Node*); typedef void (Ref::*SEL_CallFuncND)(Node*, void*); typedef void (Ref::*SEL_CallFuncO)(Ref*); typedef void (Ref::*SEL_MenuHandler)(Ref*); typedef void (Ref::*SEL_SCHEDULE)(float); #define callfunc_selector(_SELECTOR) static_cast<cocos2d::SEL_CallFunc>(&_SELECTOR) #define callfuncN_selector(_SELECTOR) static_cast<cocos2d::SEL_CallFuncN>(&_SELECTOR) #define callfuncND_selector(_SELECTOR) static_cast<cocos2d::SEL_CallFuncND>(&_SELECTOR) #define callfuncO_selector(_SELECTOR) static_cast<cocos2d::SEL_CallFuncO>(&_SELECTOR) #define menu_selector(_SELECTOR) static_cast<cocos2d::SEL_MenuHandler>(&_SELECTOR) #define schedule_selector(_SELECTOR) static_cast<cocos2d::SEL_SCHEDULE>(&_SELECTOR)
可以看到 SEL_SCHEDULE是一個關聯Ref類的函數指針定義
_selector 是一個函數,那么應該就是定時器觸發的回調函數。
TimerTargetSelector 也就是一個目標定時器,指定一個Ref對象的定時器
下面我們來看TimerTargetSelector 的幾個主要的函數。
bool TimerTargetSelector::initWithSelector(Scheduler* scheduler, SEL_SCHEDULE selector, Ref* target, float seconds, unsigned int repeat, float delay) { _scheduler = scheduler; _target = target; _selector = selector; setupTimerWithInterval(seconds, repeat, delay); return true; }
這個數不用多說,就是一個TimerTargetSelector的初始化方法。后面三個參數是用來初始化基類Timer的。
第一個參數 scheduler 因為我們還沒分析到 Scheduler類現在還不能明確它的用處,這里我們先標紅記下。
getSelector 方法不用多說,就是 _selector的 讀取方法,注意這個類沒有setSelector因為初始化 _selector要在 initWithSelector方法里進行。
接下來就是兩個重載方法 trigger 和 cancel
下面看看實現過程
void TimerTargetSelector::trigger() { if (_target && _selector) { (_target->*_selector)(_elapsed); } } void TimerTargetSelector::cancel() { _scheduler->unschedule(_selector, _target); }
實現過程非常簡單。
在trigger函數中,實際上就是調用 了初始化傳進來的回調方法。 _selector 這個回調函數接收一個參數就是度過的時間_elapsed
cancel方法中調用 了 _scheduler的 unschedule方法,這個方法怎么實現的,后面我們分析到Scheduler類的時候再細看。
小結:
TimerTargetSelector 這個類,是一個針對Ref 對象的定時器,調用的主體是這個Ref 對象。采用了回調函數來執行定時器的觸發過程。
下面我們繼續進行 閱讀 TimerTargetCallback 類的源碼
class CC_DLL TimerTargetCallback : public Timer { public: TimerTargetCallback(); /** Initializes a timer with a target, a lambda and an interval in seconds, repeat in number of times to repeat, delay in seconds. */ bool initWithCallback(Scheduler* scheduler, const ccSchedulerFunc& callback, void *target, const std::string& key, float seconds, unsigned int repeat, float delay); /** * @js NA * @lua NA */ inline const ccSchedulerFunc& getCallback() const { return _callback; }; inline const std::string& getKey() const { return _key; }; virtual void trigger() override; virtual void cancel() override; protected: void* _target; ccSchedulerFunc _callback; std::string _key; };
這個類也是 Timer 類的子類,與TimerTargetSelector類的結構類似
先看成員變量,
_target 一個void類型指針,應該是記錄一個對象的
ccSchedulerFunc 最上在定義的一個回調函數
還有一個_key 應該是一個定時器的別名。
initWithCallback 這個函數就是一些set操作來根據參數對其成員變量賦值,不用多說。
getCallback 是 _callback的讀取方法。
getkey是_key值的讀取方法。
下面我們重點看一下 trigger與 cancel的實現。
void TimerTargetCallback::trigger() { if (_callback) { _callback(_elapsed); } } void TimerTargetCallback::cancel() { _scheduler->unschedule(_key, _target); }
這兩個方法實現也很簡單,
在trigger中就是調用了callback方法並且把_elapsed作為參數 傳遞。
cancel與上面的cancel實現一樣,后面我們會重點分析 unschedule 方法。
下面一個Timer類的了類是TimerScriptHandler 與腳本調用 有關,這里大家自行看一下代碼,結構與上面的兩個類大同小異。
接下來我們碰到了本章節的主角了。 Scheduler 類
在Scheduler類之前聲明了四個結構體,我們看一眼
struct _listEntry; struct _hashSelectorEntry; struct _hashUpdateEntry; #if CC_ENABLE_SCRIPT_BINDING class SchedulerScriptHandlerEntry; #endif
后面分析Scheduler時會碰到這幾個數據類型,這幾個結構體的定義很簡單,后面碰到難點我們在詳細說。
類定義
class CC_DLL Scheduler : public Ref {
不用多說了,這樣的定義我們已經碰到好多了, Scheduler也是 Ref的了類。
老方法,先看成員變量。了解Scheduler的數據結構。
float _timeScale; // 速度控制,值為1.0f為正常速度 小於1 慢放,大於1 快放。 // // "updates with priority" stuff // struct _listEntry *_updatesNegList; // list of priority < 0 三種優先級的list具體作用這里看不出來,下面在源碼中去分析 struct _listEntry *_updates0List; // list priority == 0 struct _listEntry *_updatesPosList; // list priority > 0 struct _hashUpdateEntry *_hashForUpdates; // hash used to fetch quickly the list entries for pause,delete,etc // Used for "selectors with interval" struct _hashSelectorEntry *_hashForTimers; struct _hashSelectorEntry *_currentTarget; bool _currentTargetSalvaged; // If true unschedule will not remove anything from a hash. Elements will only be marked for deletion. bool _updateHashLocked; #if CC_ENABLE_SCRIPT_BINDING Vector<SchedulerScriptHandlerEntry*> _scriptHandlerEntries; #endif // Used for "perform Function" std::vector<std::function<void()>> _functionsToPerform; std::mutex _performMutex;
看了這些成員變量,大多是一些鏈表,數組,具體干什么的也猜不太出來,沒關系,我們從方法入手,看看都干了些什么。
構造函數 與 析構函數
Scheduler::Scheduler(void) : _timeScale(1.0f) , _updatesNegList(nullptr) , _updates0List(nullptr) , _updatesPosList(nullptr) , _hashForUpdates(nullptr) , _hashForTimers(nullptr) , _currentTarget(nullptr) , _currentTargetSalvaged(false) , _updateHashLocked(false) #if CC_ENABLE_SCRIPT_BINDING , _scriptHandlerEntries(20) #endif { // I don't expect to have more than 30 functions to all per frame _functionsToPerform.reserve(30); } Scheduler::~Scheduler(void) { unscheduleAll(); }
構造函數與析構函數都很簡單,注意構造函數里面有一行注釋,不希望在一幀里面有超過30個回調函數。我們在編寫自己的程序的時候也要注意這一點。
析構函數中調用 了 unscheduleAll 這個函數我們先不跟進看。后面再分析,這里要記住unscheduleAll是一個清理方法。
getTimeScale 與 setTimeScale 是讀寫_timeScale的方法,控制定時器速率的。
下面我們看 Scheduler::schedule 的幾個重載方法。
void Scheduler::schedule(SEL_SCHEDULE selector, Ref *target, float interval, unsigned int repeat, float delay, bool paused) { CCASSERT(target, "Argument target must be non-nullptr"); tHashTimerEntry *element = nullptr; HASH_FIND_PTR(_hashForTimers, &target, element); if (! element) { element = (tHashTimerEntry *)calloc(sizeof(*element), 1); element->target = target; HASH_ADD_PTR(_hashForTimers, target, element); // Is this the 1st element ? Then set the pause level to all the selectors of this target element->paused = paused; } else { CCASSERT(element->paused == paused, ""); } if (element->timers == nullptr) { element->timers = ccArrayNew(10); } else { for (int i = 0; i < element->timers->num; ++i) { TimerTargetSelector *timer = static_cast<TimerTargetSelector*>(element->timers->arr[i]); if (selector == timer->getSelector()) { CCLOG("CCScheduler#scheduleSelector. Selector already scheduled. Updating interval from: %.4f to %.4f", timer->getInterval(), interval); timer->setInterval(interval); return; } } ccArrayEnsureExtraCapacity(element->timers, 1); } TimerTargetSelector *timer = new TimerTargetSelector(); timer->initWithSelector(this, selector, target, interval, repeat, delay); ccArrayAppendObject(element->timers, timer); timer->release(); }
先看 schedule 方法的幾個參數 很像 TimerTargetSelector 類的init方法的幾個參數。
下面看一下schedule的函數過程,
先調用了 HASH_FIND_PTR(_hashForTimers, &target, element); 有興趣的同學可以跟一下 HASH_FIND_PTR這個宏,這行代碼的含義是在 _hashForTimers 這個數組中找與&target相等的元素,用element來返回。
而_hashForTimers不是一個數組,但它是一個線性結構的,它是一個鏈表。
下面的if判斷是判斷element的值,看看是不是已經在_hashForTimers鏈表里面,如果不在那么分配內存創建了一個新的結點並且設置了pause狀態。
再下面的if判斷的含義是,檢查當前這個_target的定時器列表狀態,如果為空那么給element->timers分配了定時器空間
如果這個_target的定時器列表不為空,那么檢查列表里是否已經存在了 selector 的回調,如果存在那么更新它的間隔時間,並退出函數。
ccArrayEnsureExtraCapacity(element->timers, 1);
這行代碼是給 ccArray分配內存,確定能再容納一個timer.
函數的最后四行代碼,就是創建了一個新的 TimerTargetSelector 對象,並且對其賦值 還加到了 定時器列表里。
這里注意一下,調用了 timer->release() 減少了一次引用,會不會造成timer被釋放呢?當然不會了,大家看一下ccArrayAppendObject方法里面已經對 timer進行了一次retain操作所以 調用了一次release后保證 timer的引用計數為1.
看過這個方法,我們清楚了幾點
- tHashTimerEntry 這個結構體是用來記錄一個Ref 對象的所有加載的定時器
- _hashForTimers 是用來記錄所有的 tHashTimerEntry 的鏈表頭指針。
下面一個 schedule函數的重載版本與第一個基本是一樣的
void Scheduler::schedule(const ccSchedulerFunc& callback, void *target, float interval, bool paused, const std::string& key) { this->schedule(callback, target, interval, kRepeatForever, 0.0f, paused, key); }
唯一 的區別是這個版本的 repeat參數為 kRepeatForever 永遠執行。
下面看第三個 schedule的重載版本
void Scheduler::schedule(const ccSchedulerFunc& callback, void *target, float interval, unsigned int repeat, float delay, bool paused, const std::string& key) { CCASSERT(target, "Argument target must be non-nullptr"); CCASSERT(!key.empty(), "key should not be empty!"); tHashTimerEntry *element = nullptr; HASH_FIND_PTR(_hashForTimers, &target, element); if (! element) { element = (tHashTimerEntry *)calloc(sizeof(*element), 1); element->target = target; HASH_ADD_PTR(_hashForTimers, target, element); // Is this the 1st element ? Then set the pause level to all the selectors of this target element->paused = paused; } else { CCASSERT(element->paused == paused, ""); } if (element->timers == nullptr) { element->timers = ccArrayNew(10); } else { for (int i = 0; i < element->timers->num; ++i) { TimerTargetCallback *timer = static_cast<TimerTargetCallback*>(element->timers->arr[i]); if (key == timer->getKey()) { CCLOG("CCScheduler#scheduleSelector. Selector already scheduled. Updating interval from: %.4f to %.4f", timer->getInterval(), interval); timer->setInterval(interval); return; } } ccArrayEnsureExtraCapacity(element->timers, 1); } TimerTargetCallback *timer = new TimerTargetCallback(); timer->initWithCallback(this, callback, target, key, interval, repeat, delay); ccArrayAppendObject(element->timers, timer); timer->release(); }
這個版本與第一個版本過程基本一樣,只不過這里使用的_target不是Ref類型而是void*類型,可以自定義類型的定時器。所以用到了TimerTargetCallback這個定時器結構。
同樣將所有 void*對象存到了 _hashForTimers
還有一個版本的 schedule 重載,它是第三個版本的擴展,擴展了重復次數為永遠。
這里小結一下 schedule方法。
Ref類型與非Ref類型對象的定時器處理基本一樣,都是加到了調度控制器的_hashForTimers鏈表里面,
調用schedule方法會將指定的對象與回調函數做為參數加到schedule的 定時器列表里面。加入的過程會做一個檢測是否重復添加的操作。
下面我們看一下幾個 unschedule 方法。unschedule方法作用是將定時器從管理列表里面刪除。
void Scheduler::unschedule(SEL_SCHEDULE selector, Ref *target) { // explicity handle nil arguments when removing an object if (target == nullptr || selector == nullptr) { return; } //CCASSERT(target); //CCASSERT(selector); tHashTimerEntry *element = nullptr; HASH_FIND_PTR(_hashForTimers, &target, element); if (element) { for (int i = 0; i < element->timers->num; ++i) { TimerTargetSelector *timer = static_cast<TimerTargetSelector*>(element->timers->arr[i]); if (selector == timer->getSelector()) { if (timer == element->currentTimer && (! element->currentTimerSalvaged)) { element->currentTimer->retain(); element->currentTimerSalvaged = true; } ccArrayRemoveObjectAtIndex(element->timers, i, true); // update timerIndex in case we are in tick:, looping over the actions if (element->timerIndex >= i) { element->timerIndex--; } if (element->timers->num == 0) { if (_currentTarget == element) { _currentTargetSalvaged = true; } else { removeHashElement(element); } } return; } } } }
我們按函數過程看,怎么來卸載定時器的。
- 參數為一個回調函數指針和一個Ref 對象指針。
- 在 對象定時器列表_hashForTimers里找是否有 target 對象
- 在找到了target對象的條件下,對target裝載的timers進行逐一遍歷
- 遍歷過程 比較當前遍歷到的定時器的 selector是等於傳入的 selctor
- 將找到的定時器從element->timers里刪除。重新設置timers列表里的 計時器的個數。
- 最后_currentTarget 與 element的比較值來決定是否從_hashForTimers 將其刪除。
這些代碼過程還是很好理解的,不過程小魚在看這幾行代碼的時候有一個問題還沒看明白,就是用到了_currentTarget 與 _currentTargetSalvaged 這兩個變量,它們的作用是什么呢?下面我們帶着這個問題來找答案。
再看另一個unschedule重載版本,基本都是大同小異,都是執行了這幾個步驟,只是查找的參數從 selector變成了 std::string &key 對象從 Ref類型變成了void*類型。
現在我們看一下update方法。當看到update方法時就知道 這個方法是在每一幀中調用的,也是引擎驅動的靈魂。
update方法的詳細分析。
void Scheduler::update(float dt) { _updateHashLocked = true;// 這里加了一個狀態鎖,應該是線程同步的作用。 if (_timeScale != 1.0f) { dt *= _timeScale;// 時間速率調整,根據設置的_timeScale 進行了乘法運算。 } // // Selector callbacks // // 定義了兩個鏈表遍歷的指針。 tListEntry *entry, *tmp; // 處理優先級小於0的定時器,這些定時器存在了_updatesNegList鏈表里面,具體怎么存進來的,目前我們還不知道,這里放出一個疑問2 DL_FOREACH_SAFE(_updatesNegList, entry, tmp) { if ((! entry->paused) && (! entry->markedForDeletion)) { entry->callback(dt);// 對活動有效的定時器執行回調。 } } // 處理優先級為0的定時器。 DL_FOREACH_SAFE(_updates0List, entry, tmp) { if ((! entry->paused) && (! entry->markedForDeletion)) { entry->callback(dt); } } // 處理優先級大於0的定時器 DL_FOREACH_SAFE(_updatesPosList, entry, tmp) { if ((! entry->paused) && (! entry->markedForDeletion)) { entry->callback(dt); } } // 遍歷_hashForTimers里自定義的計時器對象列表 for (tHashTimerEntry *elt = _hashForTimers; elt != nullptr; ) { _currentTarget = elt;// 這里通過遍歷動態設置了當前_currentTarget對象。 _currentTargetSalvaged = false;// 當前目標定時器沒有被處理過標記。 if (! _currentTarget->paused) { // 遍歷每一個對象的定時器列表 for (elt->timerIndex = 0; elt->timerIndex < elt->timers->num; ++(elt->timerIndex)) { elt->currentTimer = (Timer*)(elt->timers->arr[elt->timerIndex]);// 這里更新了對象的currentTimer elt->currentTimerSalvaged = false; elt->currentTimer->update(dt);// 執行定時器過程。 if (elt->currentTimerSalvaged) { // The currentTimer told the remove itself. To prevent the timer from // accidentally deallocating itself before finishing its step, we retained // it. Now that step is done, it's safe to release it.
// currentTimerSalvaged的作用是標記當前這個定時器是否已經失效,在設置失效的時候我們對定時器增加過一次引用記數,這里調用release來減少那次引用記數,這樣釋放很安全,這里用到了這個小技巧,延遲釋放,這樣后面的程序不會出現非法引用定時器指針而出現錯誤 elt->currentTimer->release(); } // currentTimer指針使用完了,設置成空指針 elt->currentTimer = nullptr; } } // elt, at this moment, is still valid // so it is safe to ask this here (issue #490)
// 因為下面有可能要清除這個對象currentTarget為了循環進行下去,這里先在currentTarget對象還存活的狀態下找到鏈表的下一個指針。 elt = (tHashTimerEntry *)elt->hh.next; // only delete currentTarget if no actions were scheduled during the cycle (issue #481)
// 如果_currentTartetSalvaged 為 true 且這個對象里面的定時器列表為空那么這個對象就沒有計時任務了我們要把它從__hashForTimers列表里面刪除。 if (_currentTargetSalvaged && _currentTarget->timers->num == 0) { removeHashElement(_currentTarget); } } // 下面這三個循環也是清理工作 // updates with priority < 0 DL_FOREACH_SAFE(_updatesNegList, entry, tmp) { if (entry->markedForDeletion) { this->removeUpdateFromHash(entry); } } // updates with priority == 0 DL_FOREACH_SAFE(_updates0List, entry, tmp) { if (entry->markedForDeletion) { this->removeUpdateFromHash(entry); } } // updates with priority > 0 DL_FOREACH_SAFE(_updatesPosList, entry, tmp) { if (entry->markedForDeletion) { this->removeUpdateFromHash(entry); } } _updateHashLocked = false; _currentTarget = nullptr; #if CC_ENABLE_SCRIPT_BINDING // // Script callbacks // // Iterate over all the script callbacks if (!_scriptHandlerEntries.empty()) { for (auto i = _scriptHandlerEntries.size() - 1; i >= 0; i--) { SchedulerScriptHandlerEntry* eachEntry = _scriptHandlerEntries.at(i); if (eachEntry->isMarkedForDeletion()) { _scriptHandlerEntries.erase(i); } else if (!eachEntry->isPaused()) { eachEntry->getTimer()->update(dt); } } } #endif // // 上面都是對象的定時任務, 這里是多線程處理函數的定時任務。 // // Testing size is faster than locking / unlocking. // And almost never there will be functions scheduled to be called. 這塊作者已經說明了,函數的定時任務不常用。我們簡單了解一下就可了。 if( !_functionsToPerform.empty() ) { _performMutex.lock(); // fixed #4123: Save the callback functions, they must be invoked after '_performMutex.unlock()', otherwise if new functions are added in callback, it will cause thread deadlock. auto temp = _functionsToPerform; _functionsToPerform.clear(); _performMutex.unlock(); for( const auto &function : temp ) { function(); } } }
通過上面的代碼分析我們對 schedule的update有了進一步的了解。這里的currentTartet對象我們已經了解了是什么意思。
疑問1的解答:
_currentTarget是在 update主循環過程中用來標記當前執行到哪個target的對象。
_currentTargetSalvaged 是標記_currentTarget是否需要進行清除操作的變量。
schedule這個類主要的幾個函數我們都 分析過了,下面還有一些成員方法,我們簡單說明一下,代碼都很簡單大家根據上面的分析可以自行閱讀一下。
/** 根據key與target 指針來判斷是否這個對象的這個key的定時器在Scheduled里面控制。 */ bool isScheduled(const std::string& key, void *target); /** 同上,只不過判斷條件不一樣。. @since v3.0 */ bool isScheduled(SEL_SCHEDULE selector, Ref *target); ///////////////////////////////////// /** 暫停一個對象的所有定時器 */ void pauseTarget(void *target); /** 恢復一個對象的所有定時器 */ void resumeTarget(void *target); /** 詢問一個對象的定時器是不是暫停狀態 */ bool isTargetPaused(void *target); /** 暫停所有對象的定時器 */ std::set<void*> pauseAllTargets(); /** 根據權重值來暫停所有對象的定時器 */ std::set<void*> pauseAllTargetsWithMinPriority(int minPriority); /** 恢復描寫對象的定時器暫停狀態。 */ void resumeTargets(const std::set<void*>& targetsToResume); /** 將一個函數定時器加入到調度管理器里面。 這也是update函數中最后處理的那個函數列表里的函數 任務增加的接口。 */ void performFunctionInCocosThread( const std::function<void()> &function);
到這里,疑問2 還沒有找到答案。
我們回顧一下,上一章節看Node類的源碼的時候,關於調度任務那塊的代碼我們暫時略過了,這里我們回去看一眼。
先看Node類構造函數中對調度器的初始化過程有這樣兩行代碼。
_scheduler = director->getScheduler();
_scheduler->retain();
通過這兩行代碼我們可以知道在這里沒有重新構建一個新的Scheduler而是用了Director里創建的Scheduler。而Director里面是真正創建了Scheduler對象。
我們再看Node類的一些Schedule方法。
void Node::schedule(SEL_SCHEDULE selector) { this->schedule(selector, 0.0f, kRepeatForever, 0.0f); } void Node::schedule(SEL_SCHEDULE selector, float interval) { this->schedule(selector, interval, kRepeatForever, 0.0f); } void Node::schedule(SEL_SCHEDULE selector, float interval, unsigned int repeat, float delay) { CCASSERT( selector, "Argument must be non-nil"); CCASSERT( interval >=0, "Argument must be positive"); _scheduler->schedule(selector, this, interval , repeat, delay, !_running); } void Node::scheduleOnce(SEL_SCHEDULE selector, float delay) { this->schedule(selector, 0.0f, 0, delay); } void Node::unschedule(SEL_SCHEDULE selector) { // explicit null handling if (selector == nullptr) return; _scheduler->unschedule(selector, this); } void Node::unscheduleAllSelectors() { _scheduler->unscheduleAllForTarget(this); }
看到了這些方法及實現 ,其實上面都分析過了,只不過Node 類又集成了一份,其實就是調用 了Director里的schedulor對象及相應的操作。
我們再看Node類的這兩個函數
/** * Schedules the "update" method. * * It will use the order number 0. This method will be called every frame. * Scheduled methods with a lower order value will be called before the ones that have a higher order value. * Only one "update" method could be scheduled per node. * @js NA * @lua NA */ void scheduleUpdate(void); /** * Schedules the "update" method with a custom priority. * * This selector will be called every frame. * Scheduled methods with a lower priority will be called before the ones that have a higher value. * Only one "update" selector could be scheduled per node (You can't have 2 'update' selectors). * @js NA * @lua NA */ void scheduleUpdateWithPriority(int priority);
這段注釋已經說的很清楚了,Node的這兩個方法 會在每一幀都被調用,而不是按時間間隔來定時的。
看到這段注釋,使我們對定時器的另一個調度機制有了了解,前面分析都是針對 一段間隔時間的調度機制,而這里又浮現了幀幀調度的機制。
下面我們來梳理一下。
記得 在Node類里面有一個方法 update
我們回顧一下它的聲明
/* * Update method will be called automatically every frame if "scheduleUpdate" is called, and the node is "live" */ virtual void update(float delta);
注釋寫的很清楚, 如果 scheduleUpdate方法被調用 且 node在激活狀態, 那么 update方法將會在每一幀中都會被調用
再看一下 scheduleUpdate 相關方法。
void Node::scheduleUpdate() { scheduleUpdateWithPriority(0); } void Node::scheduleUpdateWithPriority(int priority) { _scheduler->scheduleUpdate(this, priority, !_running); }
在Node類定義默認都是 0 級別的結點。
可以看到最終是調用了_scheduler->scheduleUpdate 方法,我們再跟到 Scheduler::scheduleUpdate
template <class T> void scheduleUpdate(T *target, int priority, bool paused) { this->schedulePerFrame([target](float dt){ target->update(dt); }, target, priority, paused); }
看到了吧,Node::update 會在 回調函數中被調用 ,這塊代碼有點不好理解 大家參考一下 c++11的 lambda表達式,這里的回調函數定義了一個匿名函數。函數的實現過程就是調用 target的update方法。在node類中target那塊傳遞的是node的this指針。
再看一下 schedulePerFrame方法。
void Scheduler::schedulePerFrame(const ccSchedulerFunc& callback, void *target, int priority, bool paused) { tHashUpdateEntry *hashElement = nullptr; HASH_FIND_PTR(_hashForUpdates, &target, hashElement); if (hashElement) { #if COCOS2D_DEBUG >= 1 CCASSERT(hashElement->entry->markedForDeletion,""); #endif // TODO: check if priority has changed! hashElement->entry->markedForDeletion = false; return; } // most of the updates are going to be 0, that's way there // is an special list for updates with priority 0 if (priority == 0) { appendIn(&_updates0List, callback, target, paused); } else if (priority < 0) { priorityIn(&_updatesNegList, callback, target, priority, paused); } else { // priority > 0 priorityIn(&_updatesPosList, callback, target, priority, paused); } }
哈哈,在這里將幀調度過程加入到了相應權限的調度列表中,到此疑問2已經得到了解決。
要注意的一點是,這個方法先對target做了檢測,如果已經在幀調度列表里面會直接返回的,也就是說一個node結點只能加入一次幀調度列表里,也只能有一個回調過程,這個過程就是Node::update方法,如果想實現自己的幀調度邏輯那么重載它好了。
好啦,今天羅嗦這么多,大家看的可能有些亂,小魚這里總結一下。
- Scheduler 類是cocos2d-x里的調度控制類,它分兩種調度模式 按幀調度與按時間間隔調度 ,當然,如果時間間隔設置小於幀的時間間隔那么就相當於按幀調度了。
- 按幀調度被集成在Node類里,調度的回調函數就是Node::update函數。
- 按時間調度可以分兩種形式對象形式, 一種 是Ref基類的對象,一種是任意對象。
- Scheduler實際上是存儲了很多小任務的列表管理器,每一個定時任務都是以Timer類為基類實現的。管理器的列表以對象的指針哈希存放的。
- cocos2d-x引擎啟動后Director類會創建一個默認的調度管理器,所有的Node類默認都會引入Director的調度管理器,調度管理器會在Director的 mainLoop里的 drawscene方法里被每一幀都調度。
Scheduler類我們就分析到這里,今天 的內容關聯了好幾個類,如果有什么問題可以在評論中向我提出,有好建議大家也不要吝嗇,多多向我提。
下一章我們來剖析Cocos2d-x的事件機制 Event。