schedule 詳解

Cocos2d-X3.0 刨根問底(六)----- 調度器Scheduler類源碼分析

上一章,我們分析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類定義能瞭解到的信息如下:

  1. Timer類也是Ref類的子類,採用了cocos2d-x統一的內存管理機制。
  2. 這裏一個抽象類。必須被繼承來使用。
  3. 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.

看過這個方法,我們清楚了幾點

  1. tHashTimerEntry  這個結構體是用來記錄一個Ref 對象的所有加載的定時器
  2. _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;
            }
        }
    }
}
複製代碼

我們按函數過程看,怎麼來卸載定時器的。

  1. 參數爲一個回調函數指針和一個Ref 對象指針。
  2. 在 對象定時器列表_hashForTimers裏找是否有 target 對象
  3. 在找到了target對象的條件下,對target裝載的timers進行逐一遍歷
  4. 遍歷過程 比較當前遍歷到的定時器的 selector是等於傳入的 selctor
  5. 將找到的定時器從element->timers裏刪除。重新設置timers列表裏的 計時器的個數。
  6. 最後_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方法,如果想實現自己的幀調度邏輯那麼重載它好了。

好啦,今天羅嗦這麼多,大家看的可能有些亂,小魚這裏總結一下。

  1. Scheduler 類是cocos2d-x裏的調度控制類,它分兩種調度模式 按幀調度與按時間間隔調度 ,當然,如果時間間隔設置小於幀的時間間隔那麼就相當於按幀調度了。
  2. 按幀調度被集成在Node類裏,調度的回調函數就是Node::update函數。
  3. 按時間調度可以分兩種形式對象形式, 一種 是Ref基類的對象,一種是任意對象。
  4. Scheduler實際上是存儲了很多小任務的列表管理器,每一個定時任務都是以Timer類爲基類實現的。管理器的列表以對象的指針哈希存放的。
  5. cocos2d-x引擎啓動後Director類會創建一個默認的調度管理器,所有的Node類默認都會引入Director的調度管理器,調度管理器會在Director的 mainLoop裏的 drawscene方法裏被每一幀都調度。

Scheduler類我們就分析到這裏,今天 的內容關聯了好幾個類,如果有什麼問題可以在評論中向我提出,有好建議大家也不要吝嗇,多多向我提。

下一章我們來剖析Cocos2d-x的事件機制 Event。

選擇遊戲作爲職業
原地址:http://www.cnblogs.com/mmidd/p/3768452.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章