Cocos2dx 3.x scheduler是怎么工作的

PS:由于CCNode.cpp中的关于schedule的方法最终都要调用schedule本身的方法,所以这里就不看了。

多说一句:CCNode中的_scheduler也是从Director中拿过来的。跟一篇中的_actionManager一样。



进入正题:

下面我们看看CCScheduler.h,它里面一共定义了5个类。先一点一点看:

<span style="font-size:14px;">#ifndef __CCSCHEDULER_H__
#define __CCSCHEDULER_H__
#include <functional>
#include <mutex>
#include <set>
#include "base/CCRef.h"
#include "base/CCVector.h"
#include "base/uthash.h"
NS_CC_BEGIN
class Scheduler;

typedef std::function<void(float)> ccSchedulerFunc; </span>
定义了一个返回值为void, 接收一个float参数的函数类型ccSchedulerFunc,schedule中的回调函数都是这个类型。

timer.h,这个类是一个抽象类。作为TimerTargetSelector.h和TimerTargetCallback.h还有TimerScriptHandler.h的基类。

先看一下timer.h:

<span style="font-size:14px;">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// 定义要执行的总次数,0为1次  1为2次
    float _delay;           //延时次数
    float _interval;        //执行间隔
};</span>

参数都是一些基本参数。这里不看了。


在这里我们主要看以下timer的update方法:

<span style="font-size:14px;">void Timer::update(float dt)
{
    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
            }
        }    
        else
        {//advanced usage
            _elapsed += dt;
            if (_useDelay)
            {
                if( _elapsed >= _delay )
                {
                    trigger();
                    
                    _elapsed = _elapsed - _delay;//减去延时时间,剩下的是真正的第一次触发后到现在的时间
                    _timesExecuted += 1;
                    _useDelay = false;//延时已经过去,触发了第一次, 按正常处理(不永久执行,没有延时)
                }
            }
            else                        //不适用延时
            {
                if (_elapsed >= _interval)
                {
                    trigger(); //触发函数,是一个纯虚函数,实际作用在子类方法中
                    
                    _elapsed = 0;
                    _timesExecuted += 1;

                }
            }

            if (!_runForever && _timesExecuted > _repeat)//触发次数达到重复次数
            {    //unschedule timer
                cancel(); //取消定时器
            }
        }
    }
}</span>
在这个update方法中主要调用了tigger()和cancel()方法。由于timer类中,这两个方法是纯虚函数。所以会动态调用子类中的方法。

然后看看TimerTargetSelector.h

<span style="font-size:14px;">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,float delay);
    
    inline SEL_SCHEDULE getSelector() const { return _selector; };
    
    virtual void trigger() override;
    virtual void cancel() override;
    
protected:
    Ref* _target;               //执行定时器的对象
    SEL_SCHEDULE _selector;     //执行定时器会回调的方法
};</span>
参数也很好理解。_target执行定时器的对象,_selector执行定时器的回调方法。可能会对SEL_SCHEDULE这个新类型比较陌生,我们可以看一下他的实现:

<span style="font-size:14px;">typedef void (Ref::*SEL_SCHEDULE)(float);</span>
定义了一个关联Ref类的函数指针。这个函数返回值为void,参数类型为float,SEL_SCHEDULE就是这个函数指针的类型名。

然后看一下TimerTargetSelector中的initWithSelector方法:

<span style="font-size:14px;">bool TimerTargetSelector::initWithSelector(Scheduler* scheduler,float delay)
{
    _scheduler = scheduler;
    _target = target;
    _selector = selector;
    setupTimerWithInterval(seconds,repeat,delay);
    return true;
}</span>
这个方法也就是一些简单的初始化操作。这里会调用setupTimerWithInterval这个方法,本类中没有,所以会跑到父类中找。

然后看一下TimerTargetSelector中的trigger方法:

<span style="font-size:14px;">void TimerTargetSelector::trigger()
{
    if (_target && _selector)
    {
        (_target->*_selector)(_elapsed);
    }
}</span>
这个方法很简单,执行回调函数。

再看一下cancel方法吧:

<span style="font-size:14px;">void TimerTargetSelector::cancel()
{
    _scheduler->unschedule(_selector,_target);
}</span>

现在TimerTargetSelector.h分析完了,其实很简单,就两个成员变量。主要方法就是tigger方法,他会执行回调函数。其他两个子类跟这个类大同小异.

TimerTargetCallback.h:

<span style="font-size:14px;">class CC_DLL TimerTargetCallback : public Timer
{
public:
    TimerTargetCallback();
    
    /** Initializes a timer with a target,a lambda and an interval in seconds,delay in seconds. */
    bool initWithCallback(Scheduler* scheduler,const ccSchedulerFunc& callback,void *target,const std::string& key,float delay);
    
    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;  //一个void类型指针,应该是记录一个对象的 
    ccSchedulerFunc _callback;  //回调函数
    std::string _key;           //定时器的另一个别名
};</span>
这里说一下成员变量_key表示回调函数的别名。
然后看一下 它的trigger方法:

<span style="font-size:14px;">void TimerTargetCallback::trigger()
{
    if (_callback)
    {
        _callback(_elapsed);
    }
}</span>

这里我还有一个没有弄明白的地方就是回调函数。先画个问号明天就看这个回调函数。

终于要到Scheduler.h中了,由于这里面代码比较多。我主要是看了一下成员变量还有几个主要的方法,要看成员变量,第一眼看到的就是几个结构体。

先对这几个结构体做个解析:

<span style="font-size:14px;">typedef struct _listEntry <span style="white-space:pre">		</span> //<span style="color: rgb(51,51); font-family: 'Helvetica Neue',serif; line-height: 25.2000007629395px;">这个结构体是为scheduler自带的update定义的,(定时器的基本属性)</span>
{
    struct _listEntry   *prev,*next;
    ccSchedulerFunc     callback;
    void                *target;            //key
    int                 priority;<span style="white-space:pre">	</span>    //优先级,越小越先执行
    bool                paused;
    bool                markedForDeletion; //是否需要删除
} tListEntry;</span>

<span style="font-size:14px;">typedef struct _hashUpdateEntry  <span style="white-space:pre">	</span>//<span style="color: rgb(51,serif; line-height: 25.2000007629395px;">这个结构体是为</span><span style="font-family: 'Helvetica Neue',serif; line-height: 25.2000007629395px;">scheduler自带的update定义的。</span>
{
    tListEntry          **list;        // 这里需要注意一单,这个list存放的是上边结构体的内容
    tListEntry          *entry;        // entry in the list
    void                *target;
    ccSchedulerFunc     callback;
    UT_hash_handle      hh;
} tHashUpdateEntry;</span>

typedef struct _hashSelectorEntry //这个是为Ref类型的对象和非Ref类型的对象定义的结构体(主要是为自定义的结构体用到的类型)
{
    ccArray             *timers;<span style="white-space:pre">		</span> //存放target相关的所有timer
    void                *target;<span style="white-space:pre">		</span>//开启定时器的node(一般作为key来找对应的Hash表元素)
    int                 timerIndex; <span style="white-space:pre">		</span>//当前定时器的下标
    Timer               *currentTimer; <span style="white-space:pre">		</span>//一个node可以开启多个定时器(指向当前的定时器)
    bool                currentTimerSalvaged;  //当前状态是否可回收
    bool                paused;
    UT_hash_handle      hh;
} tHashTimerEntry;

然后看一下scheduler.h中的成员变量:

    float _timeScale;
    struct _listEntry *_updatesNegList;        // list of priority < 0
    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;      //用来记录所有的 tHashTimerEntry 的链表头指针
    struct _hashSelectorEntry *_currentTarget;
    bool _currentTargetSalvaged;
    // If true unschedule will not remove anything from a hash. Elements will only be marked for deletion.
    bool _updateHashLocked;

_hashForUpdates表示的是schedule自身开启的定时器链表中的第一个元素,这个元素是_hashUpdateEntry类型的,他的list指向_updatesNegList、_updates0List、_updatesPosList。


_hashForTimers表示的是自定义的定时器链表中的第一个元素,这个元素类型是_hashSelectorEntry。

_currentTarget表示的是当前遍历到的那一个元素。

_currentTargetSalvaged表示当前指向的元素是否可以回收(为true表示可以回收)

_updateHashLocked这个东西不太懂有什么作用。英文注释说的是:如果为true在unschedule的时候不会从Hash表中移除任何一个元素,需要移除的元素会标记为可移除状态。


上边的这些成员变量我也是一直半解。


然后来看看schedule函数:

<span style="font-size:14px;">void Scheduler::schedule(SEL_SCHEDULE selector,Ref *target,float interval,float delay,bool paused)
{
    CCASSERT(target,"Argument target must be non-nullptr");
    
    //_hashForTimers 这个数组中找与&target相等的元素,用element来返回
    tHashTimerEntry *element = nullptr;
    HASH_FIND_PTR(_hashForTimers,&target,element);//根据target查找对应的hash表元素
    /*
        tHashTimerEntry  这个结构体是用来记录一个Ref 对象的所有加载的定时器
        _hashForTimers 是用来记录所有的 tHashTimerEntry 的链表头指针。
    */
    if (! element)
    {   //如果没有找到,创建一个
        element = (tHashTimerEntry *)calloc(sizeof(*element),1);
        element->target = target;
        //添加到hash表链中
        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,"");
    }
    //检查这个元素的定时器列表,如果列表为空 则new 10个数组出来备用
    if (element->timers == nullptr)
    {
        element->timers = ccArrayNew(10);
    }
    else
    {   //循环查找定时器数组,看看是不是曾经定义过相同的定时器,如果定义过,则只需要修改定时器的间隔时间
        for (int i = 0; i < element->timers->num; ++i)
        {
            TimerTargetSelector *timer = dynamic_cast<TimerTargetSelector*>(element->timers->arr[i]);
            
            if (timer && selector == timer->getSelector())
            {
                CCLOG("CCScheduler#scheduleSelector. Selector already scheduled. Updating interval from: %.4f to %.4f",timer->getInterval(),interval);
                timer->setInterval(interval);
                return;
            }
        }
        //扩展1个定时器数组   //给 ccArray分配内存,确定能再容纳一个timer
        ccArrayEnsureExtraCapacity(element->timers,1);
    }


    //创建一个定时器,并且将定时器加入到当前链表指针的定时器数组中。。创建timer并把timer添加到element->timers数组中是
    //schedule()的目的。。此时这个timer已经进入到schedule的检测数组中了。。
    TimerTargetSelector *timer = new (std::nothrow) TimerTargetSelector();
    timer->initWithSelector(this,selector,interval,delay);
    ccArrayAppendObject(element->timers,timer);
    timer->release();
}</span>

下面看一下schedule的函数过程:
先调用了 HASH_FIND_PTR(_hashForTimers,element); 有兴趣的同学可以跟一下 HASH_FIND_PTR这个宏,这行代码的含义是在 _hashForTimers 这个数组中找与&target相等的元素,用element来返回。

而_hashForTimers不是一个数组,但它是一个线性结构的,它是一个链表。

下面的if判断是判断element的值,看看是不是已经在_hashForTimers链表里面,如果不在那么分配内存创建了一个新的结点并且设置了pause状态。
再下面的if判断的含义是,检查当前这个_target的定时器列表状态,如果为空那么给element->timers分配了定时器空间
如果这个_target的定时器列表不为空,那么检查列表里是否已经存在了 selector 的回调,如果存在那么更新它的间隔时间,并退出函数。

1
ccArrayEnsureExtraCapacity(element->timers,1);//这行代码是给 ccArray分配内存,确定能再容纳一个timer。


函数的最后四行代码,就是创建了一个新的 TimerTargetSelector 对象,并且对其赋值 还加到了 定时器列表里。

这里注意一下,调用了 timer->release() 减少了一次引用,会不会造成timer被释放呢?当然不会了,大家看一下ccArrayAppendObject方法里面已经对 timer进行了一次retain操作所以 调用了一次release后保证 timer的引用计数为1。

看过这个方法,我们清楚了几点:

  1. tHashTimerEntry 这个结构体是用来记录一个Ref 对象的所有加载的定时器

  2. _hashForTimers 是用来记录所有的 tHashTimerEntry 的链表头指针。


然后看看update方法:

void Scheduler::update(float dt)
{
    _updateHashLocked = true;       // 这里加了一个状态锁,应该是线程同步的作用。

    if (_timeScale != 1.0f)
    {
        dt *= _timeScale;           // 时间速率调整,根据设置的_timeScale 进行了乘法运算。
    }
    // Selector callbacks
    // Iterate over all the Updates' selectors
    tListEntry *entry,*tmp;    //定义了两个遍历链表的指针

    // updates with priority < 0
    DL_FOREACH_SAFE(_updatesNegList,entry,tmp)
    {
        if ((! entry->paused) && (! entry->markedForDeletion))
        {
            entry->callback(dt);
        }
    }

    // updates with priority == 0
    DL_FOREACH_SAFE(_updates0List,tmp)
    {
        if ((! entry->paused) && (! entry->markedForDeletion))
        {
            entry->callback(dt);
        }
    }
    // updates with priority > 0
    DL_FOREACH_SAFE(_updatesPosList,tmp)
    {
        if ((! entry->paused) && (! entry->markedForDeletion))
        {
            entry->callback(dt);
        }
    }

    // Iterate over all the custom selectors        // 遍历_hashForTimers里自定义的计时器对象列表
    //_hashForTimers指向的是tHashTimerEntry 的链表头指针
    for (tHashTimerEntry *elt = _hashForTimers; elt != nullptr; )
    {
        _currentTarget = elt;
        _currentTargetSalvaged = false;

        if (! _currentTarget->paused)
        {
            // The 'timers' array may change while inside this loop
            //遍历每一个对象的定时器列表
            for (elt->timerIndex = 0; elt->timerIndex < elt->timers->num; ++(elt->timerIndex))
            {
                elt->currentTimer = (Timer*)(elt->timers->arr[elt->timerIndex]);
                elt->currentTimerSalvaged = false;

                elt->currentTimer->update(dt); // 执行定时器过程。在这里边可能会改变currentTimerSalvaged(例如在回调函数中关闭调度器)
                /*
                    因为TimerTargetSelector中没有update函数,这里会先调用父类即Timer中的update函数,但是穿进去的是
                    TimerTargetSelector对象的指针,在update方法中会动态的调用子类中的trigger()方法,因为在Timer类中
                    trigger()方法是virtual的
                */
                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.
                    //的作用是标记当前这个定时器是否已经失效,在设置失效的时候我们对定时器增加过一次引用记数,这里调用release来减少那次引用记数,
                    //这样释放很安全,这里用到了这个小技巧,延迟释放,这样后面的程序不会出现非法引用定时器指针而出现错误
                    elt->currentTimer->release();
                }

                elt->currentTimer = nullptr;
            }
        }

        // elt,at this moment,is still valid
        // so it is safe to ask this here (issue #490)
        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);
        }
    }

    // delete all updates that are marked for deletion

    //回收Node自带的update方法开启的调度器(如果表示为可回收)
    // updates with priority < 0
    DL_FOREACH_SAFE(_updatesNegList,tmp)
    {
        if (entry->markedForDeletion)
        {
            this->removeUpdateFromHash(entry);
        }
    }

    // updates with priority == 0
    DL_FOREACH_SAFE(_updates0List,tmp)
    {
        if (entry->markedForDeletion)
        {
            this->removeUpdateFromHash(entry);
        }
    }

    // updates with priority > 0
    DL_FOREACH_SAFE(_updatesPosList,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
    //
    // Functions allocated from another thread
    //
    // 上面都是对象的定时任务,这里是多线程处理函数的定时任务。
    // 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();
        }
        
    }
}


再看看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);
    
    //对象定时器列表_hashForTimers里找是否有 target 对象
    tHashTimerEntry *element = nullptr;
    HASH_FIND_PTR(_hashForTimers,element);
    
    if (element)
    {
        for (int i = 0; i < element->timers->num; ++i)
        {
            TimerTargetSelector *timer = static_cast<TimerTargetSelector*>(element->timers->arr[i]);
             //如果正在执行的Timer是需要被unschedule的timer,将其移除并且标识当前正在执行的Timer需要被移除状态为true。  
            /*
                在 对象定时器列表_hashForTimers里找是否有 target 对象
                在找到了target对象的条件下,对target装载的timers进行逐一遍历
                遍历过程 比较当前遍历到的定时器的 selector是等于传入的 selctor
                将找到的定时器从element->timers里删除。重新设置timers列表里的 计时器的个数。
                最后_currentTarget 与 element的比较值来决定是否从_hashForTimers 将其删除。
            */

            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--;
                }
                
                //当前timers中不再含有timer。但是如果正在执行的target是该target,则将正在执行的target将被清除标识为true  
                //否则,可以直接将其从hash中移除 
                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 将其删除。

相关文章

    本文实践自 RayWenderlich、Ali Hafizji 的文章《...
Cocos-code-ide使用入门学习地点:杭州滨江邮箱:appdevzw@1...
第一次開始用手游引擎挺激动!!!进入正题。下载资源1:从C...
    Cocos2d-x是一款强大的基于OpenGLES的跨平台游戏开发...
1.  来源 QuickV3sample项目中的2048样例游戏,以及最近《...
   Cocos2d-x3.x已经支持使用CMake来进行构建了,这里尝试...