【Cocos2d-x 3.x】 调度器Scheduler类源码分析

时间:2021-12-11 14:29:50

非个人的全部理解,部分摘自cocos官网教程,感谢cocos官网。

在<CCScheduler.h>头文件中,定义了关于调度器的五个类:Timer,TimerTargetSelector,TimerTargetCallback, TimerScriptHandler和Scheduler,Timer和Scheduler直接继承于Ref类,TimerTargetSelector,TimerTargetCallback和TimerScriptHandler继承自Timer类。

先看看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 定义要执行的次数,0表示执行1次, 1表示执行2次
float _delay; // 延迟的时间
float _interval; // 时间间隔
};

类中定义了一个Scheduler指针变量 _scheduler,根据注释可以看出,这是一个弱引用,弱引用不会增加它所引用的对象的引用计数;

根据分析Timer类的成员变量,可以知道这是一个用来描述计时器的类;

  1. 每隔 _interval 来触发一次;

  2. _useDelay可以设置定时器触是否使用延迟;
    _delay是延迟时间;

  3. _repeat可以设置定时器触发的次数; _runforever设置定时器永远执行。

然后看一下Timer类的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函数里,调用了trigger和cancel函数,trigger是触发函数,cancel是取消定时器。

然后继续看<Scheduler.h>,是TimerTargetSelector,它继承自Timer:

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; // 关联一个Ref对象,应该为执行定时器的对象
SEL_SCHEDULE _selector; // _selector是一个函数,是定时器触发时的回调函数
};

然后看看TimerTargetSelector的trigger和cancel函数,它重载了父类Timer的同名虚函数:

void TimerTargetSelector::trigger()
{
if (_target && _selector)
{
(_target->*_selector)(_elapsed);
}
} void TimerTargetSelector::cancel()
{
_scheduler->unschedule(_selector, _target);
}

可以看出,在trigger中,执行了_selector这个回调函数,cancel函数则调用了unscheduler函数来结束,稍后分析。

继续看下一个类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;
};

TimerTargetCallback 和TimerTargetSelector类似,然后可以看看它的trigger和cancel:

void TimerTargetCallback::trigger()
{
if (_callback)
{
_callback(_elapsed);
}
} void TimerTargetCallback::cancel()
{
_scheduler->unschedule(_key, _target);
}

trigger就是调用了回调函数并将_elapsed传进去,cancel和TimerTargetSelector和cancel一样。

然后还有个跟脚本相关的,暂时不会~

然后现在可以看看Scheduler类了,在Scheduler之前声明了几种结构体:

struct _listEntry;
struct _hashSelectorEntry;
struct _hashUpdateEntry; #if CC_ENABLE_SCRIPT_BINDING
class SchedulerScriptHandlerEntry;

估计和Scheduler的数据结构有关,接着看看Scheduler的数据:

    float _timeScale; // 速度控制,1.0f为正常速度,大于1表示快放,小于1表示慢放
//
// "updates with priority" stuff
//
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;
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的构造和析构函数:

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个。析构函数中调用了uncheduleAll。

然后看看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 = 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;
}
}
ccArrayEnsureExtraCapacity(element->timers, 1);
} TimerTargetSelector *timer = new (std::nothrow) TimerTargetSelector();
timer->initWithSelector(this, selector, target, interval, repeat, delay);
ccArrayAppendObject(element->timers, timer);
timer->release();
}

先调用了 HASH_FIND_PTR(_hashForTimers, &target,
element); 这行代码的含义是在  _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 的链表头指针。

void Scheduler::schedule(SEL_SCHEDULE selector, Ref *target, float interval, bool paused)
{
this->schedule(selector, target, interval, CC_REPEAT_FOREVER, 0.0f, paused);
}

这个重载版本和上一个基本类似,不同的只是设置它的执行次数为永久执行。

接着看另一个重载版本:

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 = dynamic_cast<TimerTargetCallback*>(element->timers->arr[i]); if (timer && 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 (std::nothrow) TimerTargetCallback();
timer->initWithCallback(this, callback, target, key, interval, repeat, delay);
ccArrayAppendObject(element->timers, timer);
timer->release();
}

这个重载版本跟上个基类相同,只是它使用的是void* target,上个重载版本使用的是Ref*版本, 因此这个重载版本可以自定义调度器,因此才使用了TimerTargetCallback。



小结:

Ref和非Ref类型对象的定时器处理基本一样,都加到了调度控制器的_hashFroTimers链表里;

调用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 将其删除。

其他的unschedule重载版本,基本都是大同小异,都是执行了这几个步骤,只是查找的参数从 selector变成了 std::string &key 对象从 Ref类型变成了void*类型。

现在来看一下update实现,每帧都会调用该函数,它是引擎驱动的”灵魂“:

void Scheduler::update(float dt)
{
_updateHashLocked = true;// 这里加了一个状态锁,应该是线程同步的作用。 if (_timeScale != 1.0f)
{
dt *= _timeScale;// 时间速率调整,根据设置的_timeScale 进行了乘法运算。
} //
// Selector callbacks
// // 定义了两个链表遍历的指针。
tListEntry *entry, *tmp; // 处理优先级小于0的定时器,这些定时器存在了_updatesNegList链表里面,具体怎么存进来的,在后面继续分析
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();
} }
}

在整个函数中:

_currentTarget实在update主循环过程中用来标记当前执行到哪个target对象;

_currentTargetSalvaged是标记_currentTarget是否需要进行清除操作的变量。

在Scheduler中有一个scheduleUpdate函数,什么时候调用这个呢,帧帧调用时会调用这个,来看看Node中的两个函数:

void scheduleUpdate(void);
void scheduleUpdateWithPriority(int priority);

在Node定义默认都是0级别的结点,这两个函数最后还是调用了Scheduler中的scheduleUpdate函数:

void scheduleUpdate(T *target, int priority, bool paused)
{
this->schedulePerFrame([target](float dt){
target->update(dt);
}, target, priority, paused);
}

然后来看看schedulePerFrame函数都做了些什么:

void Scheduler::schedulePerFrame(const ccSchedulerFunc& callback, void *target, int priority, bool paused)
{
tHashUpdateEntry *hashElement = nullptr;
HASH_FIND_PTR(_hashForUpdates, &target, hashElement);
if (hashElement)
{
// check if priority has changed
if ((*hashElement->list)->priority != priority)
{
if (_updateHashLocked)
{
CCLOG("warning: you CANNOT change update priority in scheduled function");
hashElement->entry->markedForDeletion = false;
hashElement->entry->paused = paused;
return;
}
else
{
// will be added again outside if (hashElement).
unscheduleUpdate(target);
}
}
else
{
hashElement->entry->markedForDeletion = false;
hashElement->entry->paused = paused;
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);
}
}

在这里可以看出将调度器添加到相应权限的列表中。

Scheduler类在官方文档的帮助下就分析了这么多,但是我知道我还是初步了解了调度器的工作原理,以后会更加努力的!