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。

时间: 2024-10-12 21:58:43

Cocos2d-X3.0 刨根问底(六)----- 调度器Scheduler类源码分析的相关文章

Cocos2d-X3.0 刨根问底(三)----- Director类源码分析

上一章我们完整的跟了一遍HelloWorld的源码,了解了Cocos2d-x的启动流程.其中Director这个类贯穿了整个Application程序,这章随小鱼一起把这个类分析透彻. 小鱼的阅读源码的习惯是,一层层地分析代码,在阅读Director这个类的时候,碰到了很多其它的Cocos2d-x类,我的方式是先大概了解一下类的作用,完整的去了解Director类,之后再去按照重要程度去分析碰到的其它类. 一点一点分析 CCDirector.h #ifndef __CCDIRECTOR_H__

监听器初始化Job、JobTracker相应TaskTracker心跳、调度器分配task源码级分析

JobTracker和TaskTracker分别启动之后(JobTracker启动流程源码级分析,TaskTracker启动过程源码级分析),taskTracker会通过心跳与JobTracker通信,并获取分配它的任务.用户将作业提交到JobTracker之后,放入相应的数据结构中,静等被分配.mapreduce job提交流程源码级分析(三)这篇文章已经分析了用户提交作业的最后步骤,主要是构造作业对应的JobInProgress并加入jobs,告知所有的JobInProgressListen

quartz集群调度机制调研及源码分析---转载

quartz2.2.1集群调度机制调研及源码分析引言quartz集群架构调度器实例化调度过程触发器的获取触发trigger:Job执行过程:总结:附: 引言 quratz是目前最为成熟,使用最广泛的java任务调度框架,功能强大配置灵活.在企业应用中占重要地位.quratz在集群环境中的使用方式是每个企业级系统都要考虑的问题.早在2006年,在ITeye上就有一篇关于quratz集群方案的讨论:http://www.iteye.com/topic/40970 ITeye创始人@Robbin在8楼

定时组件quartz系列&lt;三&gt;quartz调度机制调研及源码分析

quartz2.2.1集群调度机制调研及源码分析引言quartz集群架构调度器实例化调度过程触发器的获取触发trigger:Job执行过程:总结:附: 引言 quratz是目前最为成熟,使用最广泛的java任务调度框架,功能强大配置灵活.在企业应用中占重要地位.quratz在集群环境中的使用方式是每个企业级系统都要考虑的问题.早在2006年,在ITeye上就有一篇关于quratz集群方案的讨论:http://www.iteye.com/topic/40970 ITeye创始人@Robbin在8楼

(1)quartz集群调度机制调研及源码分析---转载

quartz2.2.1集群调度机制调研及源码分析 原文地址:http://demo.netfoucs.com/gklifg/article/details/27090179 引言quartz集群架构调度器实例化调度过程触发器的获取触发trigger:Job执行过程:总结:附: 引言 quratz是目前最为成熟,使用最广泛的java任务调度框架,功能强大配置灵活.在企业应用中占重要地位.quratz在集群环境中的使用方式是每个企业级系统都要考虑的问题.早在2006年,在ITeye上就有一篇关于qu

Cocos2d-X3.0 刨根问底(四)----- 内存管理源码分析

本系列文章发表以来得到了很多朋友的关注,小鱼在这里谢谢大家对我的支持,我会继续努力的,最近更新慢了一点,因为我老婆流产了抽了很多时间来照顾她希望大家谅解,并在此预祝我老婆早日康复. 上一篇,我们完整的分析了Director这个类,并提到了Director这个继承了Ref这个类,大致看了一下Ref这个类,是一个关于引用计数的类,从而我们可以推断Cocos2d-x用了一种引用计数的方式来管理内存对象,这一章我们刨根问底Cocos2d-x是如何实现内存管理及我们如何在实际项目开发中应用Cocos2d-

IK分词器原理与源码分析

原文:http://3dobe.com/archives/44/ 引言 做搜索技术的不可能不接触分词器.个人认为为什么搜索引擎无法被数据库所替代的原因主要有两点,一个是在数据量比较大的时候,搜索引擎的查询速度快,第二点在于,搜索引擎能做到比数据库更理解用户.第一点好理解,每当数据库的单个表大了,就是一件头疼的事,还有在较大数据量级的情况下,你让数据库去做模糊查询,那也是一件比较吃力的事(当然前缀匹配会好得多),设计上就应当避免.关于第二点,搜索引擎如何理解用户,肯定不是简单的靠匹配,这里面可以加

*CI框架装载器Loader.php源码分析

http://www.bitscn.com/pdb/php/201411/404680.html 顾名思义,装载器就是加载元素的,使用CI时,经常加载的有: $this->load->library()$this->load->view()$this->load->model()$this->load->database()$this->load->helper()$this->load->config()$this->load

SpringSecurity 5.0 认证、记住我、授权源码分析

一.SpringSecurity 过滤器链: 1.SecurityContextPersistenceFilter 会在请求开始时从配置好的SecurityContextRepository中获取SecurityContext,然后把它设置给SecurityContextHolder. 在请求完成后将SecurityContextHolder持有的SecurityContext再保存到配置好的SecurityContextRepository,同时清除SecurityContextHolder所