大佬教程收集整理的这篇文章主要介绍了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.huthash.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(); : * get interval in seconds */ inline float geTinterval() const { return _interval; }; * set interval in seconds void seTinterval(float interval) { _interval = interval; }; void setupTimerWithInterval(float seconds,unsigned int repeat,float delay); virtual void trigger() = 0; void cancel() = ; * triggers the timer */ void update( dt); : scheduler* _scheduler; // weak ref _elapsed; bool _runForever; _useDelay; unsigned int _timesExecuted; unsigned int _repeat; 0 = once,1 is 2 x executed _delay; _interval; };
第一点看过这个Timer类定义能了解到的信息如下:
初步了解之后,我们按照老方法,先看看Timer类都有哪些成员变量,了解一下它的数据结构。
第一个变量为
这是一个scheduler类的对象指针,后面有一个注释说这个指针是一个 弱引用,弱引用的意思就是,在这个指针被赋值的时候并没有增加对_scheduler的引用 计数。
后面几个变量也很好理解。
_delay; 延迟的时间 单位应该是秒 float _interval;// 时间间隔。
总结一下,通过分析Timer类的成员变量,我们可以知道这是一个用来描述一个计时器的类,
每隔 _interval 来触发一次,
可以设置定时器触发时的延迟 _useDelay和延迟时间 _delay.
可以设置定时器触发的次数_repeat 也可以设置定时器永远执行 _runforever
下面看Timer类的方法。
geTinterval 与 seTinterval不用多说了,就是_interval的 读写方法。
下面看一下 setupTimerWithInterval方法。
//repeat let the action be repeated repeat + 1 times,use kRepeatForever to let the action run conTinuously _runForever = (_repeat == kRepeatForever) ? ; }
这也是一个设置定时器属性的方法。
参数 seconds是设置了_interval
第二个参数repeat设置了重复的次数
第三个delay设置了延迟触发的时间。
通过 这三个参数的设置还计算出了几个状态变量 根据 delay是否大于0.0f计算了_useDelay
#define kRepeatForever (UINT_MAX -1)
根据 repeat值是否是 kRepeatForever来设置了 _runforever。
注意一点 第一行代码
_elapsed = -;
这说明这个函数 setupTimerWithInterval 是一个初始化的函数,将已经渡过的时间初始化为-1。所以在已经运行的定时器使用这个函数的时候计时器会重新开始。
下面看一下重要的方法 update
这个update 代码很简单,就是一个标准的定时器触发逻辑,没有接触过的同学可以试模仿一下。
在这个update方法里,调用了 trigger与 cancel方法,现在我们可以理解这两个抽象方法是个什么作用,
trigger是触发函数
cancel是取消定时器
具体怎么触发与怎么取消定时器,就要在Timer的子类里实现了。
Timer类源码我们分析到这里,下面看Timer类的第一个子类 TimerTargetSELEctor 的定义
in seconds. bool initWithSELEctor(scheduler* scheduler,SEL_scheDULE SELEctor,Ref* target,0)"> delay); inline SEL_scheDULE getSELEctor() _SELEctor; }; void trigger() overridevoid cancel() ; : Ref* _target; SEL_scheDULE _SELEctor; };
这个类也很简单。
我们先看一下成员变量 一共两个成员变量
Ref* _target;
这里关联了一个 Ref对象,应该是执行定时器的对象。
SEL_scheDULE 这里出现了一个新的类型,我们跟进一下,这个类型是在Ref类下面定义的,我们看一下。
可以看到 SEL_scheDULE是一个关联Ref类的函数指针定义
_SELEctor 是一个函数,那么应该就是定时器触发的回调函数。
TimerTargetSELEctor 也就是一个目标定时器,指定一个Ref对象的定时器
下面我们来看TimerTargetSELEctor 的几个主要的函数。
这个数不用多说,就是一个TimerTargetSELEctor的初始化方法。后面三个参数是用来初始化基类Timer的。
第一个参数 scheduler 因为我们还没分析到 scheduler类现在还不能明确它的用处,这里我们先标红记下。(scheduler是定时器管理器)
getSELEctor 方法不用多说,就是 _SELEctor的 读取方法,注意这个类没有setSELEctor因为初始化 _SELEctor要在 initWithSELEctor方法里进行。
接下来就是两个重载方法 trigger 和 cancel
下面看看实现过程
实现过程非常简单。
在trigger函数中,实际上就是调用 了初始化传进来的回调方法。 _SELEctor 这个回调函数接收一个参数就是度过的时间_elapsed
cancel方法中调用 了 _scheduler的 unschedule方法,这个方法怎么实现的,后面我们分析到scheduler类的时候再细看。
小结:
TimerTargetSELEctor 这个类,是一个针对Ref 对象的定时器,调用的主体是这个Ref 对象。采用了回调函数来执行定时器的触发过程。
下面我们继续进行 阅读 TimerTargetCallBACk 类的源码
这个类也是 Timer 类的子类,与TimerTargetSELEctor类的结构类似
先看成员变量,
_target 一个void类型指针,应该是记录一个对象的
ccschedulerFunc 在最上定义的一个回调函数
还有一个_key 应该是一个定时器的别名。(The key to identify the callBACk)
initWithCallBACk 这个函数就是一些set操作来根据参数对其成员变量赋值,不用多说。
getkey是_key值的读取方法。
下面我们重点看一下 trigger与 cancel的实现。
这两个方法实现也很简单,
在trigger中就是调用了callBACk方法并且把_elapsed作为参数 传递。
cancel与上面的cancel实现一样,后面我们会重点分析 unschedule 方法。
下面一个Timer类的了类是TimerScriptHandler 与脚本调用 有关,这里大家自行看一下代码,结构与上面的两个类大同小异。
接下来我们碰到了本章节的主角了。 scheduler 类
在scheduler类之前声明了四个结构体,我们看一眼
后面分析scheduler时会碰到这几个数据类型,这几个结构体的定义很简单,后面碰到难点我们在详细说。
类定义
不用多说了,这样的定义我们已经碰到好多了, scheduler也是 Ref的了类。
老方法,先看成员变量。了解scheduler的数据结构。
看了这些成员变量,大多是一些链表,数组,具体干什么的也猜不太出来,没关系,我们从方法入手,看看都干了些什么。
构造函数 与 析构函数
scheduler::scheduler() : _timeScale(1.0f),_updatesNegList(nullptr),_updates0List(nullptr),_updatesPosList(nullptr),_hashForupdates(nullptr),_hashForTimers(nullptr),_currentTarget(nullptr),_currentTargetSalvaged(:rgb(0,_updateHashLocked() ,_scriptHandlerEntries(20#endif { I don't expect to have more than 30 functions to all per frame _functionsToPerform.reserve(30); } scheduler::~scheduler() { unscheduleAll(); }
构造函数与析构函数都很简单,注意构造函数里面有一行注释,不希望在一帧里面有超过30个回调函数。我们在编写自己的程序的时候也要注意这一点。
析构函数中调用 了 unscheduleAll 这个函数我们先不跟进看。后面再分析,这里要记住unscheduleAll是一个清理方法。
getTimeScale 与 setTimeScale 是读写_timeScale的方法,控制定时器速率的。
下面我们看 scheduler::schedule 的几个重载方法。
先看 schedule 方法的几个参数 很像 TimerTargetSELEctor 类的init方法的几个参数。
下面看一下schedule的函数过程,
先调用了 HASH_FIND_PTR(_hashForTimers,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.
看过这个方法,我们清楚了几点
下面一个 schedule函数的重载版本与第一个基本是一样的
唯一 的区别是这个版本的 repeat参数为 kRepeatForever 永远执行。
下面看第三个 schedule的重载版本
这个版本与第一个版本过程基本一样,只不过这里使用的_target不是Ref类型而是void*类型,可以自定义类型的定时器。所以用到了TimerTargetCallBACk这个定时器结构。
同样将所有 void*对象存到了 _hashForTimers
还有一个版本的 schedule 重载,它是第三个版本的扩展,扩展了重复次数为永远。
这里小结一下 schedule方法。
Ref类型与非Ref类型对象的定时器处理基本一样,都是加到了调度控制器的_hashForTimers链表里面,
调用schedule方法会将指定的对象与回调函数做为参数加到schedule的 定时器列表里面。加入的过程会做一个检测是否重复添加的操作。
(转者理解:schedule管理每个对象的定时器列表,Ref类型对象的定时器是TimerTargetSELEctor类型,非Ref类型对象的定时器是TimerTargetCallBACk类型,每个对象都对应一个数据结构tHashTimerEntry,这个结构体是用来记录一个对象的所有加载的定时器,定时器的区别主要在于回调函数。故当你调用schedule方法为一个对象添加一个定时器时,首先找到该对象对应的数据结构tHashTimerEntry,在判断数据结构中是否存在回调函数等同于形参中的回调函数的定时器,有则修改定时器的属性值interval等,没有则new一个与对象对应的定时器,利用形参初始化定时器的回调函数及其他属性值,加入该对象的定时器列表tHashTimerEntry中)
下面我们看一下几个 unschedule 方法。unschedule方法作用是将定时器从管理列表里面删除。
我们按函数过程看,怎么来卸载定时器的。
这些代码过程还是很好理解的,不过程小鱼在看这几行代码的时候有一个问题还没看明白,就是用到了_currentTarget 与 _currentTargetSalvaged 这两个变量,它们的作用是什么呢?下面我们带着这个问题来找答案。
再看另一个unschedule重载版本,基本都是大同小异,都是执行了这几个步骤,只是查找的参数从 SELEctor变成了 std::string &key 对象从 Ref类型变成了void*类型。
现在我们看一下update方法。当看到update方法时就知道 这个方法是在每一帧中调用的,也是引擎驱动的灵魂。
update方法的详细分析。
// 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 == ) { removeHasHelement(_currentTarget); } } 下面这三个循环也是清理工作 updates with priority < 0 if (entry->@H_663_18@markedFordeletion) { this->removeupdateFromHash(entry); } } updates with priority == 0 updates with priority > 0 removeupdateFromHash(entry); } } _updateHashLocked = ; _currentTarget = nullptr; #if CC_ENABLE_SCRIPT_BINDING Script callBACks Iterate over all the script callBACks _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(void *target); * 同上,只不过判断条件不一样。. @since v3.0 bool isscheduled(SEL_scheDULE SELEctor,0)">target); ///////////////////////////////////// * 暂停一个对象的所有定时器 void pauseTarget(target); * 恢复一个对象的所有定时器 void resumeTarget(* 询问一个对象的定时器是不是暂停状态 bool isTargetPaused(* 暂停所有对象的定时器 std::set<void*> pauseAllTargets(); * 根据权重值来暂停所有对象的定时器 void*> pauseAllTargetsWithMinPriority( minPriority); * 恢复描写对象的定时器暂停状态。 void resumeTargets(void*>& targetsToResumE); * 将一个函数定时器加入到调度管理器里面。 这也是update函数中最后处理的那个函数列表里的函数 任务增加的接口。 void performFunctionInCocosThread( const std::function<void()> &function);
到这里,疑问2 还没有找到答案。
我们回顾一下,上一章节看Node类的源码的时候,关于调度任务那块的代码我们暂时略过了,这里我们回去看一眼。
先看Node类构造函数中对调度器的初始化过程有这样两行代码。
通过这两行代码我们可以知道在这里没有重新构建一个新的scheduler而是用了Director里创建的scheduler。而Director里面是真正创建了scheduler对象。
我们再看Node类的一些schedule方法。
看到了这些方法及实现 ,其实上面都分析过了,只不过Node 类又集成了一份,其实就是调用 了Director里的schedulor对象及相应的操作。
我们再看Node类的这两个函数
这段注释已经说的很清楚了,Node的这两个方法 会在每一帧都被调用,而不是按时间间隔来定时的。
看到这段注释,使我们对定时器的另一个调度机制有了了解,前面分析都是针对 一段间隔时间的调度机制,而这里又浮现了帧帧调度的机制。
下面我们来梳理一下。
我们回顾一下它的声明
注释写的很清楚, 如果 scheduleupdate方法被调用 且 node在激活状态,那么 update方法将会在每一帧中都会被调用
在Node类定义默认都是 0 级别的结点。
可以看到最终是调用了_scheduler->scheduleupdate 方法,我们再跟到 scheduler::scheduleupdate
template <class T> void scheduleupdate(T *target,255)">int priority,0)"> paused) { this->schedulePerFrame([target]( dt){ target->update(dt); },paused); }
看到了吧,Node::update 会在 回调函数中被调用 ,这块代码有点不好理解 大家参考一下 c++11的 lambda表达式,这里的回调函数定义了一个匿名函数。函数的实现过程就是调用 target的update方法。在Node类中target那块传递的是node的this指针。
再看一下 schedulePerFrame方法。
哈哈,在这里将帧调度过程加入到了相应权限的调度列表中,到此疑问2已经得到了解决。
要注意的一点是,这个方法先对target做了检测,如果已经在帧调度列表里面会直接返回的,也就是说一个node结点只能加入一次帧调度列表里,也只能有一个回调过程,这个过程就是Node::update方法,如果想实现自己的帧调度逻辑那么重载它好了。
好啦,今天罗嗦这么多,大家看的可能有些乱,小鱼这里总结一下。
scheduler类我们就分析到这里,今天 的内容关联了好几个类,如果有什么问题可以在评论中向我提出,有好建议大家也不要吝啬,多多向我提。
下一章我们来剖析Cocos2d-x的事件机制 Event。
以上是大佬教程为你收集整理的Cocos2d-X3.0 刨根问底(六)----- 调度器Scheduler类源码分析全部内容,希望文章能够帮你解决Cocos2d-X3.0 刨根问底(六)----- 调度器Scheduler类源码分析所遇到的程序开发问题。
如果觉得大佬教程网站内容还不错,欢迎将大佬教程推荐给程序员好友。
本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。